ADDED data/skin definitions/default.txt Index: data/skin definitions/default.txt ================================================================== --- data/skin definitions/default.txt +++ data/skin definitions/default.txt @@ -0,0 +1,1605 @@ +Font: Small = DroidSans_8 +Font: Normal = DroidSans_11 +Font: Bold = DroidSans_11_Bold +Font: Italic = OpenSans_11_Italic +Font: Medium = DroidSans_16 +Font: Big = DroidSans_20 + +Font: Subsys = DroidSans_11 +Font: Name = GoodTimes_9 +Font: Detail = DroidSans_8 + +Font: Title = GoodTimes_18 +Font: Subtitle = DroidSans_14 +Font: Button = DroidSans_11 + +Color: Text = #fff +Color: Selected = #a0000080 +Color: Disabled = #888888ff + +Color: ButtonText = #fff + +// {{{ Generic Styles +Style: RoundedBox + Element: Normal + Rect: [3,3][182,39] + Margin: 6 + Vertical: Scaled + Horizontal: Scaled + + Element: Hovered + Rect: [3,43][182,79] + Margin: 6 + Vertical: Scaled + Horizontal: Scaled + + Element: Active + Rect: [3,83][182,119] + Margin: 6 + Vertical: Scaled + Horizontal: Scaled + + Element: Hovered, Active + Inherit: RoundedBox, Active + + Element: Disabled + Rect: [195,343][374,379] + Margin: 6 + Vertical: Scaled + Horizontal: Scaled + +Style: StraightBox + Element: Normal + Rect: [194,4][373,39] + Margin: 6 + Vertical: Scaled + Horizontal: Scaled + + Element: Active + Rect: [194,84][373,119] + Margin: 6 + Vertical: Scaled + Horizontal: Scaled + + Element: Hovered + Rect: [194,44][373,79] + Margin: 6 + Vertical: Scaled + Horizontal: Scaled + + Element: Active, Hovered + Rect: [194,124][373,159] + Margin: 6 + Vertical: Scaled + Horizontal: Scaled + +Style: TinyBox + Element: Normal + Rect: [194,4][373,39] + Margin: 2 + Vertical: Scaled + Horizontal: Scaled + + Element: Hovered + Rect: [194,84][373,119] + Margin: 2 + Vertical: Scaled + Horizontal: Scaled + +Style: ItemBox + Element: Normal + Rect: [3,124][182,159] + Margin: 4 + Vertical: Scaled + Horizontal: Scaled + + Element: Hovered + Rect: [3,164][182,199] + Margin: 4 + Vertical: Scaled + Horizontal: Scaled + + Element: Active + Rect: [3,204][182,239] + Margin: 4 + Vertical: Scaled + Horizontal: Scaled + + Element: Hovered, Active + Rect: [3,699][182,734] + Margin: 4 + Vertical: Scaled + Horizontal: Scaled + + Element: Disabled + Rect: [3,124][182,159] + Margin: 4 + Vertical: Scaled + Horizontal: Scaled + + Add Gradient: + TopLeft : #00000020 + TopRight: #00000020 + BotLeft : #00000020 + BotRight: #00000020 + + GX1: 0 + GY1: 0 + GX2: 100% + GY2: 100% + +Style: InputBox + Element: Normal + Rect: [2,493][181,528] + Margin: 6 + Vertical: Scaled + Horizontal: Scaled + + Element: Active + Rect: [2,533][181,568] + Margin: 6 + Vertical: Scaled + Horizontal: Scaled + + Element: Hovered + Rect: [2,573][181,608] + Margin: 6 + Vertical: Scaled + Horizontal: Scaled + + Element: Active, Hovered + Rect: [2,613][181,648] + Margin: 6 + Vertical: Scaled + Horizontal: Scaled + + Element: Disabled + Rect: [2,653][181,692] + Margin: 6 + Vertical: Scaled + Horizontal: Scaled + +Style: PlainBox + Element: Normal + Rect: [400,40][426,61] + Margin: 2 + + Horizontal: Scaled + Vertical: Scaled + +Style: PlainNameBox + Element: Normal + Rect: [429,42][459,56] + Margin: 16,14 + Horizontal: Scaled + +Style: HexPattern + Element: Normal + Rect: [390,11][412,36] + Margin: 1 + Vertical: Tiled + Horizontal: Tiled + +Style: SmallHexPattern + Element: Normal + Rect: [390,40][395,56] + Margin: 0 + Vertical: Tiled + Horizontal: Tiled + +Style: PatternBox + Element: Normal + Layer: PlainBox + Layer: SmallHexPattern + Color Override: #ffffff40 + OX1: +3 + OY1: +3 + OX2: -3 + OY2: -3 + + GradientMode: Overlay + Add Gradient: + TopLeft : #4443 + TopRight: #4443 + BotLeft : #2221 + BotRight: #2221 + + GX1: +3 + GY1: +3 + GX2: -3 + GY2: -3 + +Style: Panel + Element: Normal + Rect: [195,179][375,215] + Margin: 6 + + Vertical: Scaled + Horizontal: Scaled + + Add Gradient: + TopLeft : #1b1b1bff + TopRight: #1b1b1bff + BotLeft : #101010ff + BotRight: #101010ff + + GX1: +4 + GY1: 50% + GX2: -5 + GY2: -5 + +Style: HorizBar + Element: Normal + Rect: [205,221][365,257] + Margin: 6 + + Vertical: Scaled + Horizontal: Scaled + +Style: PlainOverlay + Element: Normal + Rect: [400,40][426,61] + Margin: 2 + + Horizontal: Scaled + Vertical: Scaled + + Add Gradient: + TopLeft : #1c1c1cff + TopRight: #1c1c1cff + BotLeft : #151515ff + BotRight: #151515ff + + GX1: 1 + GY1: 1 + GX2: -1 + GY2: -1 + + Element: Hovered + Rect: [400,40][426,61] + Margin: 2 + + Horizontal: Scaled + Vertical: Scaled + + Add Gradient: + TopLeft : #2c2c2cff + TopRight: #2c2c2cff + BotLeft : #202020ff + BotRight: #202020ff + + GX1: 1 + GY1: 1 + GX2: -1 + GY2: -1 + + Element: Disabled + Inherit: PlainOverlay, Normal + +Style: OverlayBox + Element: Normal + Inherit: Panel + +Style: LightPanel + Element: Normal + Rect: [195,221][375,257] + Margin: 6 + + Vertical: Scaled + Horizontal: Scaled + +Style: Highlight + Element: Normal + Rect: [5,740][182,773] + Margin: 6 + + Vertical: Scaled + Horizontal: Scaled + +Style: Glow + Element: Normal + Rect: [4,780][184,849] + Margin: 10 + + Vertical: Scaled + Horizontal: Scaled + +Style: SubtleGlow + Element: Normal + Rect: [4,854][184,922] + Margin: 10 + + Vertical: Scaled + Horizontal: Scaled + +Style: HighlightPanel + Element: Normal + Inherit: Panel, Normal + + Element: Hovered + Inherit: Panel, Normal + + Add Gradient: + TopLeft : #2c2c2c40 + TopRight: #2c2c2c40 + BotLeft : #20202040 + BotRight: #20202040 + + GX1: 3 + GY1: 3 + GX2: -3 + GY2: -3 + +Style: RoundedTitle + Element: Normal + Rect: [195,264][393,294] + Margin: 6, 6, 120, 6 + + Horizontal: Scaled + Vertical: Scaled + +Style: CenterTitle + Element: Normal + Rect: [195,308][410,334] + Margin: 90, 6 + + Horizontal: Scaled + Vertical: Scaled + +Style: FullTitle + Element: Normal + Rect: [198,450][332,480] + Margin: 6 + + Horizontal: Scaled + Vertical: Scaled + +Style: PanelTitle + Element: Normal + Layer: FullTitle + Color Override: #fff + OX1: 0 + OY1: 0 + OX2: 100% + OY2: 100% + Layer: RoundedTitle + OX1: 0 + OY1: 0 + OX2: 70% + OY2: 100% + +Style: WindowTitle + Inherit: FullTitle + +Style: SubTitle + Element: Normal + Rect: [201,487][327,517] + Margin: 6 + + Horizontal: Scaled + Vertical: Scaled + +Style: HorizAccent + Element: Normal + Rect: [197,385][302,439] + Margin: 8 + + Horizontal: Scaled + Vertical: Scaled +// }}} +// {{{ Generic Parts +Style: DownArrow + Element: Normal + Rect: [443,6][455,19] + +Style: UpArrow + Element: Normal + Rect: [457,6][470,19] + +Style: RightArrow + Element: Normal + Rect: [429,6][441,19] + +Style: LeftArrow + Element: Normal + Rect: [416,6][428,19] + +Style: Field + Inherit: PlainBox + +Style: FieldName + Inherit: PlainNameBox + +Style: BG3D + Element: Normal + Rect: [202,526][446,636] + +Style: ProgressBarBG + Element: Normal + Rect: [382,70][488,80] + Margin: 6, 4 + Horizontal: Scaled + Vertical: Scaled + +Style: ProgressBar + Element: Normal + Rect: [383,83][487,91] + Margin: 3, 4 + Horizontal: Scaled + Vertical: Scaled + +Style: DragHandle + Element: Normal + Inherit: ItemBox, Active + +Style: ResizeHandle + Element: Normal + Inherit: ItemBox, Normal + Element: Active + Inherit: ItemBox, Active + +Style: Checkmark + Element: Normal + Rect: [419,23][430,34] +// }}} +// {{{ Basic GUI Elements +Style: Button + Inherit: RoundedBox + +Style: BaselineButton + Element: Normal + Rect: [3,3][182,35] + Margin: 6 + Vertical: Scaled + Horizontal: Scaled + + Element: Hovered + Rect: [3,43][182,75] + Margin: 6 + Vertical: Scaled + Horizontal: Scaled + + Element: Active + Rect: [3,83][182,115] + Margin: 6 + Vertical: Scaled + Horizontal: Scaled + + Element: Hovered, Active + Inherit: BaselineButton, Active + + Element: Disabled + Rect: [195,343][374,375] + Margin: 6 + Vertical: Scaled + Horizontal: Scaled + +Style: IconButton + Element: Normal + Element: Hovered + Inherit: Highlight + Element: Active + Inherit: Highlight + Element: Hovered,Active + Inherit: Highlight + +Style: IconToggle + Element: Normal + Element: Hovered + Inherit: Highlight + Element: Active + Inherit: Highlight + Layer: Button + Element: Hovered,Active + Inherit: Highlight + Layer: Button + +Style: Tab + Inherit: Button + +Style: PageTab + Element: Normal + Layer: Button + Element: Hovered + Layer: Button + Layer: SubtleGlow + Color Override: #ffffff + Element: Active + Layer: Button + Layer: SubtleGlow + +Style: Listbox + +Style: ListboxItem + Inherit: ItemBox + +Style: StaticListboxItem + Element: Normal + Inherit: ItemBox, Normal + +Style: DropdownList + +Style: DropdownListItem + Element: Normal + Inherit: ItemBox + + Element: Hovered + Inherit: ItemBox, Active + + Element: Active + Inherit: ItemBox, Active + + Element: Active, Hovered + Inherit: ItemBox, Active, Hovered + +Style: Dropdown + Inherit: RoundedBox + +Style: DropdownArrow + Element: Normal + Layer: DownArrow + OX1: +7 + OY1: +7 + OX2: -7 + OY2: -7 + +Style: Dialog + Element: Normal + Inherit: Panel + +Style: Tooltip + Element: Normal + Rect: [2,454][181,489] + Margin: 6 + + Horizontal: Scaled + Vertical: Scaled + +Style: Textbox + Inherit: InputBox + + Element: Focused + Inherit: InputBox, Active + +Style: HoverTextbox + Element: Normal + Element: Disabled + Element: Disabled, Hovered + Element: Hovered + Inherit: InputBox + Element: Focused + Inherit: InputBox + Element: Hovered, Focused + Inherit: InputBox, Active + +Style: HoverButton + Element: Normal + Element: Disabled + Element: Disabled, Hovered + Element: Active + Inherit: Button, Active + Element: Hovered + Inherit: Button + Element: Hovered, Active + Inherit: Button, Active + +Style: GlowButton + Element: Normal + Inherit: PlainBox + Element: Hovered + Layer: PlainBox + Layer: Highlight + Color Override: #00c0ff + Element: Hovered, Disabled + Inherit: PlainBox + Element: Active + GradientMode: Overlay + Layer: PlainBox + + Add Gradient: + TopLeft : #2228 + TopRight: #2228 + BotLeft : #00c0ff48 + BotRight: #00c0ff48 + + GX1: 0 + GY1: 0 + GX2: 100% + GY2: 100% + Element: Active, Hovered + Layer: GlowButton, Active + Layer: Highlight + Color Override: #00c0ff + +Style: TabButton + Element: Normal + GradientMode: Overlay + Layer: PlainBox + + Add Gradient: + TopLeft : #4448 + TopRight: #4448 + BotLeft : #2228 + BotRight: #2228 + + GX1: 1 + GY1: 1 + GX2: -1 + GY2: -1 + Element: Hovered + GradientMode: Overlay + Layer: TabButton, Normal + Add Gradient: + TopLeft : #fff0 + TopRight: #fff0 + BotLeft : #fff3 + BotRight: #fff3 + + GX1: 1 + GY1: 1 + GX2: -1 + GY2: -1 + Element: Active + GradientMode: Overlay + Layer: PlainBox + Color Override: #00c0ff + + Add Gradient: + TopLeft : #2228 + TopRight: #2228 + BotLeft : #00c0ff48 + BotRight: #00c0ff48 + + GX1: 1 + GY1: 1 + GX2: -1 + GY2: -1 + Element: Active, Hovered + GradientMode: Overlay + Layer: TabButton, Active + Add Gradient: + TopLeft : #fff0 + TopRight: #fff0 + BotLeft : #fff2 + BotRight: #fff2 + + GX1: 1 + GY1: 1 + GX2: -1 + GY2: -1 + +Style: AccordionHeader + Element: Normal + Inherit: PatternBox + + Element: Hovered + Inherit: PatternBox + Layer: Highlight + + Element: Active + Inherit: PatternBox + + Element: Hovered, Active + Inherit: PatternBox + Layer: Highlight + + Element: Disabled + GradientMode: Overlay + + Add Gradient: + TopLeft : #2221 + TopRight: #2221 + BotLeft : #4446 + BotRight: #4446 + + GX1: 0 + GY1: 0 + GX2: 100% + GY2: -2 + + Add Gradient: + TopLeft : #444f + TopRight: #444f + BotLeft : #444f + BotRight: #444f + + GX1: 0 + GY1: -2 + GX2: 100% + GY2: 100% + +Style: BuildElement + Element: Normal + GradientMode: Overlay + + Add Gradient: + TopLeft : #0000 + TopRight: #0000 + BotLeft : #2224 + BotRight: #2224 + + GX1: 0 + GY1: 0 + GX2: 100% + GY2: -1 + + Add Gradient: + TopLeft : #222f + TopRight: #222f + BotLeft : #222f + BotRight: #222f + + GX1: 0 + GY1: -1 + GX2: 100% + GY2: 100% + + Element: Hovered + GradientMode: Overlay + + Add Gradient: + TopLeft : #222a + TopRight: #222a + BotLeft : #444a + BotRight: #444a + + GX1: 0 + GY1: 0 + GX2: 100% + GY2: -1 + + Add Gradient: + TopLeft : #222f + TopRight: #222f + BotLeft : #222f + BotRight: #222f + + GX1: 0 + GY1: -1 + GX2: 100% + GY2: 100% + +Style: SpinButton + Inherit: TinyBox + +Style: ContextMenu + +Style: ContextMenuItem + Element: Normal + Inherit: ItemBox + + Element: Hovered + Inherit: ItemBox, Active + + Element: Active + Inherit: ItemBox, Active + + Element: Active, Hovered + Inherit: ItemBox, Active + +Style: Checkbox + Element: Normal + Inherit: ItemBox + + Element: Hovered + Inherit: ItemBox, Hovered + + Element: Active + Layer: ItemBox, Active + + Layer: Checkmark + OX1: 4 + OY1: 0 + OX2: 100% + OY2: -4 + + Element: Active, Hovered + Layer: ItemBox, Active, Hovered + + Layer: Checkmark + OX1: 4 + OY1: 0 + OX2: 100% + OY2: -4 + +Style: Radiobox + Element: Normal + Inherit: ItemBox + + Element: Hovered + Inherit: ItemBox, Hovered + + Element: Active + Layer: ItemBox + + Layer: ItemBox, Active + OX1: 20% + OY1: 20% + OX2: -20% + OY2: -20% + + Element: Active, Hovered + Layer: ItemBox, Hovered + + Layer: ItemBox, Active + OX1: 20% + OY1: 20% + OX2: -20% + OY2: -20% + +Style: ScrollVert + Inherit: Panel + +Style: ScrollVertHandle + Element: Normal + Inherit: StraightBox + + Element: Hovered + Inherit: StraightBox, Hovered + + Element: Active + Inherit: StraightBox, Active + +Style: ScrollHoriz + Inherit: Panel + +Style: ScrollHorizHandle + Element: Normal + Inherit: StraightBox + + Element: Hovered + Inherit: StraightBox, Hovered + + Element: Active + Inherit: StraightBox, Active + +Style: ScrollButton + Inherit: Button + +Style: ScrollUp + Element: Normal + Layer: ScrollButton + Layer: UpArrow + OX1: +4 + OY1: +4 + OX2: -4 + OY2: -4 + + Element: Hovered + Layer: ScrollButton, Hovered + Layer: UpArrow + OX1: +4 + OY1: +4 + OX2: -4 + OY2: -4 + + Element: Active + Layer: ScrollButton, Active + Layer: UpArrow + OX1: +4 + OY1: +4 + OX2: -4 + OY2: -4 + +Style: ScrollDown + Element: Normal + Layer: ScrollButton + Layer: DownArrow + OX1: +4 + OY1: +4 + OX2: -4 + OY2: -4 + + Element: Hovered + Layer: ScrollButton, Hovered + Layer: DownArrow + OX1: +4 + OY1: +4 + OX2: -4 + OY2: -4 + + Element: Active + Layer: ScrollButton, Active + Layer: DownArrow + OX1: +4 + OY1: +4 + OX2: -4 + OY2: -4 + +Style: ScrollLeft + Element: Normal + Layer: ScrollButton + Layer: LeftArrow + OX1: +4 + OY1: +4 + OX2: -4 + OY2: -4 + + Element: Hovered + Layer: ScrollButton, Hovered + Layer: LeftArrow + OX1: +4 + OY1: +4 + OX2: -4 + OY2: -4 + + Element: Active + Layer: ScrollButton, Active + Layer: LeftArrow + OX1: +4 + OY1: +4 + OX2: -4 + OY2: -4 + +Style: ScrollRight + Element: Normal + Layer: ScrollButton + Layer: RightArrow + OX1: +4 + OY1: +4 + OX2: -4 + OY2: -4 + + Element: Hovered + Layer: ScrollButton, Hovered + Layer: RightArrow + OX1: +4 + OY1: +4 + OX2: -4 + OY2: -4 + + Element: Active + Layer: ScrollButton, Active + Layer: RightArrow + OX1: +4 + OY1: +4 + OX2: -4 + OY2: -4 + +Style: DistributionBar + Inherit: StraightBox + +Style: DistributionElement + Element: Normal + GradientMode: Overlay + Add Gradient: + TopLeft : #bbb9 + TopRight: #bbb9 + BotLeft : #4449 + BotRight: #4449 + + GX1: 0 + GY1: 0 + GX2: 100% + GY2: 100% + +Style: ChoiceBox + Inherit: ItemBox + +// }}} +// {{{ Tab Bar +Style: GameTabBar + Element: Normal + Rect: [1,400] [100,424] + Margin: 0 + + Vertical: Scaled + Horizontal: Scaled + +Style: GameTab + Element: Normal + Rect: [2,345] [86,371] + Margin: 17, 0 + Horizontal: Scaled + + Element: Hovered + Rect: [2,317] [86,343] + Margin: 17, 0 + Horizontal: Scaled + + Element: Pressed + Inherit: GameTab, Normal + + Element: Active + Rect: [2,372] [86,398] + AspectMargin: Horizontal + Margin: 17,0 + Horizontal: Scaled + +Style: GameTabClose + Element: Normal + Rect: [3,429][17,443] + + Element: Hovered + Rect: [17,429][31,443] + + Element: Active + Rect: [32,429][46,443] + + +Style: GameTabNew + Element: Normal + Rect: [138,350][169,369] + Margin: 9 + Horizontal: Scaled + + Element: Hovered + Rect: [106,350][136,369] + Margin: 9 + Horizontal: Scaled + + Element: Active + Rect: [138,373][169,392] + Margin: 9 + Horizontal: Scaled + +Style: HomeIcon + Element: Normal + Rect: [105, 300][136, 319] + + Element: Hovered + Rect: [138, 300][169, 319] + + Element: Active + Rect: [105, 324][136,343] + +Style: GoIcon + Element: Normal + Rect: [105,253][136,272] + + Element: Hovered + Rect: [138,253][169,272] + + Element: Active + Rect: [105,277][136,296] + +Style: GalaxyIcon + Element: Normal + Rect: [132,428] [157,451] + + Vertical: Uniform + Horizontal: Uniform + +Style: PlanetIcon + Inherit: GalaxyIcon + +Style: SupportIcon + Inherit: GalaxyIcon + +Style: SystemIcon + Inherit: GalaxyIcon + +Style: DesignsIcon + Element: Normal + Rect: [94,429] [119,451] + + Vertical: Uniform + Horizontal: Uniform + +Style: ResearchIcon + Element: Normal + Rect: [62,428] [87,451] + + Vertical: Uniform + Horizontal: Uniform + +Style: GlobalBar + Element: Normal + Rect: [107,374] [136,425] + Margin: 4 + + Vertical: Scaled + Horizontal: Scaled + + Add Gradient: + TopLeft : #1b1b1b44 + TopRight: #1b1b1b44 + BotLeft : #00000044 + BotRight: #00000044 + + GX1: 1 + GY1: 1 + GX2: -1 + GY2: -1 + +Style: GoDialog + Element: Normal + Inherit: Panel + +Style: GoItem + Element: Normal + Inherit: ItemBox + + Element: Active + Inherit: ItemBox, Active + + Element: Hovered + Inherit: ItemBox, Hovered + + Element: Active, Hovered + Inherit: ItemBox, Active, Hovered +// }}} +// {{{ Global Bar +Style: BudgetProgress + Inherit: ProgressBarBG + +Style: BudgetProgressBar + Element: Normal + Inherit: ProgressBar + +Style: ResearchProgress + Inherit: ProgressBarBG + +Style: ResearchProgressBar + Element: Normal + Inherit: ProgressBar + +Style: Notification + Element: Normal + Inherit: PlainBox + +Style: TimeDisplay + Element: Normal + Inherit: PlainBox + + Layer: SmallHexPattern + OX1: +3 + OY1: +3 + OX2: -3 + OY2: -3 +// }}} +// {{{ AI Empire Tab +Style: AIEmpireBG + Element: Normal + Inherit: HexPattern + + Add Gradient: + TopLeft : #333f + TopRight: #333f + BotLeft : #000e + BotRight: #000e + + GX1: 0% + GY1: 0% + GX2: 100% + GY2: 100% + +Style: ResearchField + Inherit: HighlightPanel +// }}} +// {{{ Research Tab +Style: ResearchBG + Element: Normal + Inherit: HexPattern + + Add Gradient: + TopLeft : #212e + TopRight: #212e + BotLeft : #000e + BotRight: #000e + + GX1: 0% + GY1: 0% + GX2: 100% + GY2: 100% + +Style: ResearchField + Inherit: HighlightPanel +// }}} +// {{{ Design Tabs +Style: DesignOverviewBG + Element: Normal + Inherit: HexPattern + + Add Gradient: + TopLeft : #112e + TopRight: #112e + BotLeft : #000e + BotRight: #000e + + GX1: 0% + GY1: 0% + GX2: 100% + GY2: 100% + +Style: DesignClassHeader + Element: Normal + Inherit: PanelTitle + +Style: DesignClass + Element: Normal + Inherit: Panel + +Style: DesignBorder + Element: Normal + Inherit: Panel + Element: Hovered + Inherit: Panel + + Add Gradient: + TopLeft : #1112 + TopRight: #1112 + BotLeft : #aaa2 + BotRight: #aaa2 + + GX1: +4 + GY1: +4 + GX2: -4 + GY2: -4 + + Element: Active + Inherit: Panel + + Add Gradient: + TopLeft : #fff3 + TopRight: #fff3 + BotLeft : #fff3 + BotRight: #fff3 + + GX1: 0 + GY1: 0 + GX2: 100% + GY2: +4 + + Add Gradient: + TopLeft : #fff3 + TopRight: #fff3 + BotLeft : #fff3 + BotRight: #fff3 + + GX1: 0 + GY1: -4 + GX2: 100% + GY2: 100% + + Add Gradient: + TopLeft : #fff3 + TopRight: #fff3 + BotLeft : #fff3 + BotRight: #fff3 + + GX1: 0 + GY1: +4 + GX2: +4 + GY2: -4 + + Add Gradient: + TopLeft : #fff3 + TopRight: #fff3 + BotLeft : #fff3 + BotRight: #fff3 + + GX1: -4 + GY1: +4 + GX2: 100% + GY2: -4 + + Element: Hovered, Active + Inherit: DesignBorder, Active + + Add Gradient: + TopLeft : #1112 + TopRight: #1112 + BotLeft : #aaa2 + BotRight: #aaa2 + + GX1: +4 + GY1: +4 + GX2: -4 + GY2: -4 + +Style: DesignGradient + Element: Normal + GradientMode: Overlay + Add Gradient: + TopLeft : #aaa3 + TopRight: #8883 + BotLeft : #8883 + BotRight: #2223 + + GX1: 0 + GY1: 0 + GX2: 100% + GY2: 100% + +Style: DesignSummary + Element: Normal + Layer: DesignBorder, Normal + Color Override: #fff + Layer: DesignGradient + OX1: +4 + OY1: +4 + OX2: -4 + OY2: -4 + Layer: PlainBox + OX1: 2 + OY1: -34 + OX2: -2 + OY2: -2 + +Style: DesignEditorBG + Inherit: DesignOverviewBG + +Style: DesignNavigationClass + Inherit: LightPanel + +Style: DesignNavigationIcon + Inherit: RoundedBox + +Style: ModuleButton + Inherit: Button +// }}} +// {{{ Diplomacy Tabs +Style: DiplomacyBG + Element: Normal + Inherit: HexPattern + + Add Gradient: + TopLeft : #121e + TopRight: #121e + BotLeft : #000e + BotRight: #000e + + GX1: 0% + GY1: 0% + GX2: 100% + GY2: 100% + +Style: EmpireBox + Element: Normal + Layer: RoundedBox + + Layer: Panel + Color Override: #fff + OX1: +4 + OY1: +4 + OX2: -4 + OY2: -4 + +Style: PlayerEmpireBox + Inherit: EmpireBox + +Style: DelegationBox + Element: Normal + Layer: RoundedBox + Color Override: #fff + OX1: 0 + OY1: 4 + OX2: 100% + OY2: -4 + + Layer: RoundedBox + OX1: 0 + OY1: 0 + OX2: -46 + OY2: 100% + +Style: VotingBox + Element: Normal + Inherit: PlainBox + +Style: VoteTotal + Element: Normal + Layer: PlainBox + +Style: InfluenceVoteBox + Inherit: PatternBox + +Style: TreatyBox + Inherit: PatternBox + +Style: InfluenceEffectBox + Inherit: PatternBox +// }}} +// {{{ Planet Tab +Style: QueueBackground + Inherit: SmallHexPattern + +Style: ConstructionBox + Inherit: Panel + +Style: PlanetBox + Element: Normal + Layer: RoundedBox + Color Override: #ffffff80 + +Style: PlanetElement + Element: Normal + Layer: Panel + OX1: 0 + OY1: 0 + OX2: 100% + OY2: 100% + + Layer: PlainBox + OX1: +4 + OY1: +4 + OX2: -4 + OY2: -4 +// }}} +// {{{ System Tab +Style: SystemListBG + Element: Normal + Inherit: HexPattern + + Add Gradient: + TopLeft : #210e + TopRight: #210e + BotLeft : #000e + BotRight: #000e + + GX1: 0% + GY1: 0% + GX2: 100% + GY2: 100% + +Style: SystemPanel + Element: Normal + Layer: Panel + Color Override: #fff + + Layer: PanelTitle + OX1: +1 + OY1: +1 + OX2: -2 + OY2: +31 + +Style: PlanetBar + Element: Normal + Inherit: PlainBox + +// }}} +// {{{ Support Tab +Style: GroupPanel + Inherit: Panel + +Style: GroupSupportClass + Inherit: PatternBox +// }}} +// {{{ Wiki Tab +Style: WikiBG + Element: Normal + Inherit: HexPattern + + Add Gradient: + TopLeft : #211e + TopRight: #211e + BotLeft : #000e + BotRight: #000e + + GX1: 0% + GY1: 0% + GX2: 100% + GY2: 100% +// }}} +// {{{ Popups +Style: PopupBG + Element: Normal + Layer: Panel + Color Override: #fff + + Layer: SubTitle + OX1: +2 + OY1: +1 + OX2: -3 + OY2: 25 + + Layer: BG3D + OX1: +3 + OY1: +24 + OX2: -4 + OY2: -35 + +Style: ShipPopupBG + Element: Normal + Layer: Panel + Color Override: #fff + + Layer: SubTitle + OX1: +2 + OY1: +1 + OX2: -3 + OY2: 25 + + Layer: BG3D + OX1: +3 + OY1: +24 + OX2: -4 + OY2: -80 + +Style: GenericPopupBG + Element: Normal + Layer: Panel + Color Override: #fff + + Layer: SubTitle + OX1: +2 + OY1: +1 + OX2: -3 + OY2: 25 + + Layer: BG3D + OX1: +3 + OY1: +24 + OX2: -4 + OY2: -4 + +Style: SelectablePopup + Inherit: PopupBG + +Style: ManageButton + Element: Normal + Rect: [203,644][280,664] +// }}} +// {{{ Info Bar +Style: InfoBar + Element: Normal + Inherit: PlainBox + +Style: InfoBarPlain + Element: Normal + Layer: Panel + Color Override: #fff + OX1: 0 + OY1: 0 + OX2: 100% + OY2: 130% +// }}} +// {{{ Main Menu +Style: MapSelectorItem + Element: Normal + Inherit: Panel + + Element: Hovered + Inherit: Panel, Active + +Style: EmpireSetupItem + Element: Normal + Layer: Panel + OX1: 0 + OY1: 0 + OX2: 100% + OY2: 100% + +Style: GalaxySetupItem + Element: Normal + Layer: PatternBox + OX1: 0 + OY1: 0 + OX2: 100% + OY2: 100% + Layer: Panel + OX1: 0 + OY1: 0 + OX2: 100% + OY2: 40 + +Style: MainMenuPanel + Element: Normal + Layer: Panel + Color Override: #ffffffff + +Style: MainMenuDescPanel + Inherit: MainMenuPanel + +Style: MainMenuItem + Element: Normal + Element: Hovered + Inherit: ItemBox, Active + Element: Active + Inherit: PlainBox, Normal + + Add Gradient: + TopLeft : #aa444440 + TopRight: #aa444440 + BotLeft : #aa444440 + BotRight: #aa444440 + + GX1: 0 + GY1: 0 + GX2: 100% + GY2: 100% + + Element: Hovered, Active + Inherit: ItemBox, Active +// }}} ADDED logo.png Index: logo.png ================================================================== --- logo.png +++ logo.png cannot compute difference between binary files ADDED modinfo.txt Index: modinfo.txt ================================================================== --- modinfo.txt +++ modinfo.txt @@ -0,0 +1,34 @@ + Name: AI Empire + Compatibility: 200 + Description: << + Forces the AI on for player Empires, allows configuration of player AI (difficulty, cheats) at the new game screen, and allows runtime configuration of what roles the AI manages. + + [h1]Current version: 0.4.3[/h1] + + [h1][b]Defects[/b][/h1] + - I'm having a lot of trouble preventing all the construction that the AI wants to do. In particular it buys a factory right away and it'll fill up fleets for you. + - The tab UI hasn't gotten a lot of love yet. + - This probably doesn't save/load well. + - This probably doesn't work with multiplayer well. + + [h1][b]Changelog[/b][/h1] + - 0.4.3 - clean up the UI a bit. controllable: scuttling. + - 0.4.2 - controllable: artifact use + - 0.4.1 - controls are now also in AI's race and FTL components + - 0.4.0 - controllable: construction. 'diplomacy' now includes treaties and war/peace declarations. + - controllable: scouting and also (Anomaly Resolution) whether scouts will decide for themselves which option to pick after scanning an anomaly. + - 0.3.3 - minor fixes, depend on Missing Expansion rather than directly on the community patch + - 0.3.0 - added 'prevent achievements' button. Doesn't turn cheats on, but flags game as having ever had cheats on. + - 0.2.0 - first version with AI controls + - 'AI Empire' tab added + - controllable: diplomacy, colonization, remnant hunting, research + also, defense of systems and attacks on enemy systems when at war + - all disabled initially + - 0.1.0 - initial version, AI is on for player but you have no control over it + >> + Derives From: Missing Expansion + Override: scripts/menu/new_game.as + Override: scripts/server/empire_ai/EmpireAI.as + Override: scripts/server/empire_ai/weasel + Override: scripts/server/cheats.as + Override: data/skin definitions/default.txt ADDED scripts/gui/tabs/AIEmpireTab.as Index: scripts/gui/tabs/AIEmpireTab.as ================================================================== --- scripts/gui/tabs/AIEmpireTab.as +++ scripts/gui/tabs/AIEmpireTab.as @@ -0,0 +1,266 @@ +import tabs.Tab; +import elements.GuiButton; +import elements.GuiPanel; +import elements.GuiMarkupText; +import icons; +from tabs.tabbar import newTab, switchToTab; + +const Color colorForbidden = Color(0xaaaaaaff); +const Color colorAllowed = colors::Energy; + +Tab@ createAIEmpireTab() { + return AIEmpireTab(); +} + +void init() { + Tab@ tab = createAIEmpireTab(); + newTab(tab); + cheatCommandAI(playerEmpire, "forbid all"); +} + +class AIEmpireTab : Tab { + GuiPanel@ panel; + GuiButton@ forbidDiplomacyButton; + GuiMarkupText@ forbidDiplomacyText; + bool forbidDiplomacy; + GuiButton@ forbidColonizationButton; + GuiMarkupText@ forbidColonizationText; + bool forbidColonization; + GuiButton@ forbidCreepingButton; + GuiMarkupText@ forbidCreepingText; + bool forbidCreeping; + GuiButton@ forbidResearchButton; + GuiMarkupText@ forbidResearchText; + bool forbidResearch; + GuiButton@ forbidDefenseButton; + GuiMarkupText@ forbidDefenseText; + bool forbidDefense; + GuiButton@ forbidAttackButton; + GuiMarkupText@ forbidAttackText; + bool forbidAttack; + GuiButton@ forbidConstructionButton; + GuiMarkupText@ forbidConstructionText; + bool forbidConstruction; + GuiButton@ forbidScoutingButton; + GuiMarkupText@ forbidScoutingText; + bool forbidScouting; + GuiButton@ forbidAnomalyChoiceButton; + GuiMarkupText@ forbidAnomalyChoiceText; + bool forbidAnomalyChoice; + GuiButton@ forbidArtifactButton; + GuiMarkupText@ forbidArtifactText; + bool forbidArtifact; + GuiButton@ forbidScuttleButton; + GuiMarkupText@ forbidScuttleText; + bool forbidScuttle; + GuiButton@ preventAchievementsButton; + GuiMarkupText@ preventAchievementsText; + bool preventAchievements; + + GuiButton@ enableAIButton; + GuiMarkupText@ enableAIText; + GuiButton@ disableAIButton; + GuiMarkupText@ disableAIText; + + AIEmpireTab() { + super(); + title = "AI Empire"; + + forbidDiplomacy = true; + forbidColonization = true; + forbidCreeping = true; + forbidResearch = true; + forbidDefense = true; + forbidAttack = true; + forbidConstruction = true; + forbidScouting = true; + forbidAnomalyChoice = true; + forbidArtifact = true; + forbidScuttle = true; + preventAchievements = false; + + @panel = GuiPanel(this, Alignment()); + + @forbidDiplomacyButton = GuiButton(panel, recti_area(15,50, 120,25)); + @forbidDiplomacyText = GuiMarkupText(forbidDiplomacyButton, Alignment(Left, Top, Right, Bottom)); + forbidDiplomacyText.text = "[center]Diplomacy[/center]"; + forbidDiplomacyButton.color = colorForbidden; + + @preventAchievementsButton = GuiButton(panel, recti_area(0,370, 200,25)); + @preventAchievementsText = GuiMarkupText(preventAchievementsButton, Alignment(Left, Top, Right, Bottom)); + preventAchievementsText.text = "[center]Prevent Achievements[/center]"; + preventAchievementsButton.color = colorForbidden; + + @forbidColonizationButton = GuiButton(panel, recti_area(15,80, 120,25)); + @forbidColonizationText = GuiMarkupText(forbidColonizationButton, Alignment(Left, Top, Right, Bottom)); + forbidColonizationText.text = "[center]Colonization[/center]"; + forbidColonizationButton.color = colorForbidden; + + @forbidCreepingButton = GuiButton(panel, recti_area(15,110, 120,25)); + @forbidCreepingText = GuiMarkupText(forbidCreepingButton, Alignment(Left, Top, Right, Bottom)); + forbidCreepingText.text = "[center]Remnant Hunting[/center]"; + forbidCreepingButton.color = colorForbidden; + + @forbidResearchButton = GuiButton(panel, recti_area(15,140, 120,25)); + @forbidResearchText = GuiMarkupText(forbidResearchButton, Alignment(Left, Top, Right, Bottom)); + forbidResearchText.text = "[center]Research[/center]"; + forbidResearchButton.color = colorForbidden; + + @forbidDefenseButton = GuiButton(panel, recti_area(15,230, 120,25)); + @forbidDefenseText = GuiMarkupText(forbidDefenseButton, Alignment(Left, Top, Right, Bottom)); + forbidDefenseText.text = "[center]Wartime Defense[/center]"; + forbidDefenseButton.color = colorForbidden; + + @forbidAttackButton = GuiButton(panel, recti_area(15,200, 120,25)); + @forbidAttackText = GuiMarkupText(forbidAttackButton, Alignment(Left, Top, Right, Bottom)); + forbidAttackText.text = "[center]Wartime Offense[/center]"; + forbidAttackButton.color = colorForbidden; + + @forbidConstructionButton = GuiButton(panel, recti_area(15+200,50, 120,25)); + @forbidConstructionText = GuiMarkupText(forbidConstructionButton, Alignment(Left, Top, Right, Bottom)); + forbidConstructionText.text = "[center]Construction[/center]"; + forbidConstructionButton.color = colorForbidden; + + @forbidScoutingButton = GuiButton(panel, recti_area(15,170, 120,25)); + @forbidScoutingText = GuiMarkupText(forbidScoutingButton, Alignment(Left, Top, Right, Bottom)); + forbidScoutingText.text = "[center]Scouting[/center]"; + forbidScoutingButton.color = colorForbidden; + + @forbidAnomalyChoiceButton = GuiButton(panel, recti_area(15,260, 200,25)); + @forbidAnomalyChoiceText = GuiMarkupText(forbidAnomalyChoiceButton, Alignment(Left, Top, Right, Bottom)); + forbidAnomalyChoiceText.text = "[center]Anomaly Resolution[/center]"; + forbidAnomalyChoiceButton.color = colorForbidden; + + @forbidArtifactButton = GuiButton(panel, recti_area(15,290, 120,25)); + @forbidArtifactText = GuiMarkupText(forbidArtifactButton, Alignment(Left, Top, Right, Bottom)); + forbidArtifactText.text = "[center]Artifact Use[/center]"; + forbidArtifactButton.color = colorForbidden; + + @forbidScuttleButton = GuiButton(panel, recti_area(15,320, 120,25)); + @forbidScuttleText = GuiMarkupText(forbidScuttleButton, Alignment(Left, Top, Right, Bottom)); + forbidScuttleText.text = "[center]Scuttling[/center]"; + forbidScuttleButton.color = colorForbidden; + + @enableAIButton = GuiButton(panel, recti_area(0,0, 60,25)); + @enableAIText = GuiMarkupText(enableAIButton, Alignment(Left, Top, Right, Bottom)); + enableAIText.text = "[center]All on[/center]"; + enableAIButton.color = colorAllowed; + + @disableAIButton = GuiButton(panel, recti_area(70,0, 60,25)); + @disableAIText = GuiMarkupText(disableAIButton, Alignment(Left, Top, Right, Bottom)); + disableAIText.text = "[center]All off[/center]"; + disableAIButton.color = colorForbidden; + } + + void tick(double time) override { + } + + bool onGuiEvent(const GuiEvent& event) { + if (event.type == GUI_Clicked) { + if (event.caller is enableAIButton) { + cheatCommandAI(playerEmpire, "allow all"); + setAll(colorAllowed, false); + return true; + } + else if (event.caller is disableAIButton) { + cheatCommandAI(playerEmpire, "forbid all"); + setAll(colorForbidden, true); + return true; + } + else if (event.caller is forbidDiplomacyButton) { + toggle(forbidDiplomacyButton, forbidDiplomacy, "Diplomacy"); + return true; + } + else if (event.caller is forbidColonizationButton) { + toggle(forbidColonizationButton, forbidColonization, "Colonization"); + return true; + } + else if (event.caller is forbidCreepingButton) { + toggle(forbidCreepingButton, forbidCreeping, "Creeping"); + return true; + } + else if (event.caller is forbidResearchButton) { + toggle(forbidResearchButton, forbidResearch, "Research"); + return true; + } + else if (event.caller is forbidDefenseButton) { + toggle(forbidDefenseButton, forbidDefense, "Defense"); + return true; + } + else if (event.caller is forbidAttackButton) { + toggle(forbidAttackButton, forbidAttack, "Attack"); + return true; + } + else if (event.caller is forbidConstructionButton) { + toggle(forbidConstructionButton, forbidConstruction, "Construction"); + return true; + } + else if (event.caller is forbidScoutingButton) { + toggle(forbidScoutingButton, forbidScouting, "Scouting"); + return true; + } + else if (event.caller is forbidAnomalyChoiceButton) { + toggle(forbidAnomalyChoiceButton, forbidAnomalyChoice, "AnomalyChoice"); + return true; + } + else if (event.caller is forbidArtifactButton) { + toggle(forbidArtifactButton, forbidArtifact, "Artifact"); + return true; + } + else if (event.caller is forbidScuttleButton) { + toggle(forbidScuttleButton, forbidScuttle, "Scuttle"); + return true; + } + else if (event.caller is preventAchievementsButton) { + cheatCommandAI(playerEmpire, "no achievements"); + preventAchievementsButton.color = colorAllowed; + preventAchievements = true; + return true; + } + } + return BaseGuiElement::onGuiEvent(event); + } + + void toggle (GuiButton& btn, bool& flag, string cmd) { + if (flag) { + btn.color = colorAllowed; + cheatCommandAI(playerEmpire, "allow " + cmd); + flag = false; + } else { + btn.color = colorForbidden; + cheatCommandAI(playerEmpire, "forbid " + cmd); + flag = true; + } + } + + void setAll(Color color, bool b) { + forbidDiplomacy = b; + forbidColonization = b; + forbidCreeping = b; + forbidResearch = b; + forbidDefense = b; + forbidAttack = b; + forbidConstruction = b; + forbidScouting = b; + forbidAnomalyChoice = b; + forbidArtifact = b; + forbidScuttle = b; + forbidDiplomacyButton.color = color; + forbidColonizationButton.color = color; + forbidCreepingButton.color = color; + forbidResearchButton.color = color; + forbidDefenseButton.color = color; + forbidAttackButton.color = color; + forbidConstructionButton.color = color; + forbidScoutingButton.color = color; + forbidAnomalyChoiceButton.color = color; + forbidArtifactButton.color = color; + forbidScuttleButton.color = color; + } + + void draw() { + skin.draw(SS_AIEmpireBG, SF_Normal, AbsolutePosition); + Tab::draw(); + } +} + ADDED scripts/menu/new_game.as Index: scripts/menu/new_game.as ================================================================== --- scripts/menu/new_game.as +++ scripts/menu/new_game.as @@ -0,0 +1,3231 @@ +import menus; +import elements.BaseGuiElement; +import elements.GuiButton; +import elements.GuiPanel; +import elements.GuiOverlay; +import elements.GuiSprite; +import elements.GuiText; +import elements.GuiTextbox; +import elements.GuiSpinbox; +import elements.GuiCheckbox; +import elements.GuiDropdown; +import elements.GuiContextMenu; +import elements.GuiIconGrid; +import elements.GuiEmpire; +import elements.GuiMarkupText; +import elements.MarkupTooltip; +import elements.GuiBackgroundPanel; +import dialogs.SaveDialog; +import dialogs.LoadDialog; +import dialogs.MessageDialog; +import dialogs.QuestionDialog; +import util.settings_page; +import empire_data; +import traits; +import icons; +from util.draw_model import drawLitModel; + +import void showMultiplayer() from "multiplayer_menu"; + +from maps import Map, maps, mapCount, getMap; + +import settings.game_settings; +import util.game_options; + +const int EMPIRE_SETUP_HEIGHT = 96; +const int GALAXY_SETUP_HEIGHT = 200; + +const int REC_MAX_PEREMP = 25; +const int REC_MAX_OPTIMAL = 150; +const int REC_MAX_BAD = 400; +const int REC_MAX_OHGOD = 1000; + +const array QDIFF_COLORS = {Color(0x00ff00ff), Color(0x1197e0ff), Color(0xff0000ff)}; +const array QDIFF_NAMES = {locale::AI_DIFF_EASY, locale::AI_DIFF_NORMAL, locale::AI_DIFF_HARD}; +const array QDIFF_DESC = {locale::AI_DIFF_EASY_DESC, locale::AI_DIFF_NORMAL_DESC, locale::AI_DIFF_HARD_DESC}; +const array QDIFF_ICONS = {Sprite(spritesheet::AIDifficulty, 0), Sprite(spritesheet::AIDifficulty, 1), Sprite(spritesheet::AIDifficulty, 2)}; + +NameGenerator empireNames; +bool empireNamesInitialized = false; + +class ConfirmStart : QuestionDialogCallback { + void questionCallback(QuestionDialog@ dialog, int answer) { + if(answer == QA_Yes) { + new_game.start(); + hideNewGame(true); + } + } +}; + +class NewGame : BaseGuiElement { + GameSettings settings; + + GuiBackgroundPanel@ empireBG; + GuiBackgroundPanel@ gameBG; + GuiBackgroundPanel@ chatBG; + + GuiButton@ backButton; + GuiButton@ inviteButton; + GuiButton@ playButton; + + EmpirePortraitCreation portraits; + + int nextEmpNum = 1; + GuiPanel@ empirePanel; + EmpireSetup@[] empires; + GuiButton@ addAIButton; + + GuiSkinElement@ gameHeader; + GuiButton@ mapsButton; + array settingsButtons; + array settingsPanels; + GuiButton@ resetButton; + + GuiPanel@ galaxyPanel; + GalaxySetup@[] galaxies; + GuiButton@ addGalaxyButton; + + GuiPanel@ mapPanel; + GuiText@ mapHeader; + GuiListbox@ mapList; + + GuiPanel@ chatPanel; + GuiMarkupText@ chatLog; + GuiTextbox@ chatBox; + + bool animating = false; + bool hide = false; + bool fromMP = false; + bool choosingMap = false; + + string chatMessages; + + NewGame() { + super(null, recti()); + + @empireBG = GuiBackgroundPanel(this, Alignment( + Left+0.05f, Top+0.1f, Left+0.5f-6, Bottom-0.1f)); + empireBG.title = locale::MENU_EMPIRES; + empireBG.titleColor = Color(0x00ffe9ff); + + @gameBG = GuiBackgroundPanel(this, Alignment( + Left+0.5f+6, Top+0.1f, Left+0.95f, Bottom-0.1f)); + + @gameHeader = GuiSkinElement(gameBG, Alignment(Left+1, Top+1, Right-2, Top+41), SS_FullTitle); + + @mapsButton = GuiButton(gameHeader, Alignment(Left, Top+1, Width=200, Height=38)); + mapsButton.text = locale::MENU_GALAXIES; + mapsButton.buttonIcon = Sprite(material::SystemUnderAttack); + mapsButton.toggleButton = true; + mapsButton.font = FT_Medium; + mapsButton.pressed = true; + mapsButton.style = SS_TabButton; + + @chatBG = GuiBackgroundPanel(this, Alignment( + Left+0.05f, Bottom-0.1f-250, Left+0.5f-6, Bottom-0.1f)); + chatBG.title = locale::CHAT; + chatBG.titleColor = Color(0xff8000ff); + chatBG.visible = false; + + //Empire list + @empirePanel = GuiPanel(empireBG, + Alignment(Left, Top+34, Right, Bottom-4)); + + //Game settings + for(uint i = 0, cnt = GAME_SETTINGS_PAGES.length; i < cnt; ++i) { + auto@ panel = GuiPanel(gameBG, + Alignment(Left, Top+46, Right, Bottom-40)); + panel.visible = false; + settingsPanels.insertLast(panel); + + auto@ page = GAME_SETTINGS_PAGES[i]; + page.create(panel); + + auto@ button = GuiButton(gameHeader, Alignment(Left+200+(i*200), Top+1, Width=200, Height=38)); + button.text = page.header; + button.buttonIcon = page.icon; + button.toggleButton = true; + button.pressed = false; + button.font = FT_Medium; + button.style = SS_TabButton; + settingsButtons.insertLast(button); + } + + @resetButton = GuiButton(gameBG, Alignment(Left+0.5f-120, Bottom-40, Width=240, Height=35), locale::NG_RESET); + resetButton.color = Color(0xff8080ff); + resetButton.buttonIcon = icons::Reset; + resetButton.visible = false; + + //Galaxy list + @galaxyPanel = GuiPanel(gameBG, + Alignment(Left, Top+46, Right, Bottom-4)); + galaxyPanel.visible = false; + + @addGalaxyButton = GuiButton(galaxyPanel, + recti_area(vec2i(), vec2i(260, 36)), + locale::ADD_GALAXY); + addGalaxyButton.buttonIcon = Sprite(spritesheet::CardCategoryIcons, 3); + + //Maps choice list + @mapPanel = GuiPanel(gameBG, + Alignment(Left, Top+46, Right, Bottom-4)); + mapPanel.visible = true; + choosingMap = true; + + @mapHeader = GuiText(mapPanel, Alignment(Left, Top, Right, Top+30)); + mapHeader.font = FT_Medium; + mapHeader.horizAlign = 0.5; + mapHeader.stroke = colors::Black; + mapHeader.text = locale::CHOOSE_MAP; + + @mapList = GuiListbox(mapPanel, + Alignment(Left+4, Top+34, Right-4, Bottom-4)); + mapList.itemStyle = SS_DropdownListItem; + mapList.itemHeight = 100; + + updateMapList(); + + //Chat + @chatPanel = GuiPanel(chatBG, Alignment(Left+8, Top+34, Right-8, Bottom-38)); + @chatLog = GuiMarkupText(chatPanel, recti_area(0, 0, 100, 100)); + @chatBox = GuiTextbox(chatBG, Alignment(Left+6, Bottom-36, Right-6, Bottom-6)); + + //Actions + @playButton = GuiButton(this, Alignment( + Right-0.05f-200, Bottom-0.1f+6, Width=200, Height=46), + locale::START_GAME); + playButton.buttonIcon = Sprite(spritesheet::MenuIcons, 9); + + @addAIButton = GuiButton(this, Alignment( + Left+300, Bottom-0.1f+6, Width=200, Height=46), + locale::ADD_AI); + addAIButton.buttonIcon = icons::Add; + + @backButton = GuiButton(this, Alignment( + Left+0.05f, Bottom-0.1f+6, Width=200, Height=46), + locale::BACK); + backButton.buttonIcon = Sprite(spritesheet::MenuIcons, 11); + + @inviteButton = GuiButton(this, Alignment( + Left+0.05f+408, Bottom-0.1f+6, Width=200, Height=46), + locale::INVITE_FRIEND); + inviteButton.buttonIcon = Sprite(spritesheet::MenuIcons, 13); + inviteButton.visible = cloud::inLobby; + + updateAbsolutePosition(); + } + + void updateMapList() { + mapList.clearItems(); + for(uint i = 0, cnt = mapCount; i < cnt; ++i) { + auto@ mp = getMap(i); + if(mp.isUnique) { + bool found = false; + for(uint i = 0, cnt = galaxies.length; i < cnt; ++i) { + if(galaxies[i].mp.id == mp.id) { + found = true; + break; + } + } + if(found) + continue; + } + if(mp.isListed && !mp.isScenario && (mp.dlc.length == 0 || hasDLC(mp.dlc))) + mapList.addItem(MapElement(mp)); + } + } + + void init() { + if(!empireNamesInitialized) { + empireNames.read("data/empire_names.txt"); + empireNames.useGeneration = false; + empireNamesInitialized = true; + } + + portraits.reset(); + clearEmpires(); + addEmpire(true, getRacePreset(0)); + if(!mpServer && !fromMP) { + addEmpire(false); + addEmpire(false); + RaceChooser(empires[0], true); + } + updateAbsolutePosition(); + + switchPage(0); + + if(fromMP) { + mapPanel.visible = false; + galaxyPanel.visible = true; + choosingMap = false; + } + else { + mapPanel.visible = true; + galaxyPanel.visible = false; + choosingMap = true; + updateMapList(); + } + + addGalaxyButton.visible = !fromMP; + addAIButton.visible = !fromMP; + chatMessages = ""; + + if(fromMP) { + playButton.text = locale::MP_NOT_READY; + playButton.color = colors::Orange; + } + else { + playButton.text = locale::START_GAME; + playButton.color = colors::White; + } + } + + void addChat(const string& str) { + chatMessages += str+"\n"; + bool wasBottom = chatPanel.vert.pos >= (chatPanel.vert.end - chatPanel.vert.page); + chatLog.text = chatMessages; + chatPanel.updateAbsolutePosition(); + if(wasBottom) { + chatPanel.vert.pos = max(0.0, chatPanel.vert.end - chatPanel.vert.page); + chatPanel.updateAbsolutePosition(); + } + } + + void resetAIColors() { + for(uint i = 0, cnt = empires.length; i < cnt; ++i) { + auto@ setup = empires[i]; + if(setup.player) + continue; + setup.settings.color = colors::Invisible; + } + for(uint i = 0, cnt = empires.length; i < cnt; ++i) { + auto@ setup = empires[i]; + if(setup.player) + continue; + setUniqueColor(setup); + } + } + + void resetAIRaces() { + for(uint i = 0, cnt = empires.length; i < cnt; ++i) { + auto@ setup = empires[i]; + if(setup.player) + continue; + setup.settings.raceName = ""; + } + for(uint i = 0, cnt = empires.length; i < cnt; ++i) { + auto@ setup = empires[i]; + if(setup.player) + continue; + setup.applyRace(getUniquePreset()); + } + } + + RacePreset@ getUniquePreset() { + uint index = randomi(0, getRacePresetCount() - 1); + for(uint i = 0, cnt = getRacePresetCount(); i < cnt; ++i) { + auto@ preset = getRacePreset((index+i) % cnt); + if(preset.dlc.length != 0 && !hasDLC(preset.dlc)) + continue; + bool has = false; + for(uint n = 0, ncnt = empires.length; n < ncnt; ++n) { + if(empires[n].settings.raceName == preset.name) { + has = true; + break; + } + } + if(!has) { + return preset; + } + } + for(uint i = 0, cnt = getRacePresetCount(); i < cnt; ++i) { + auto@ preset = getRacePreset((index+i) % cnt); + if(preset.dlc.length != 0 && !hasDLC(preset.dlc)) + continue; + return preset; + } + return getRacePreset(index); + } + + void setUniqueColor(EmpireSetup@ setup) { + bool found = false; + Color setColor; + for(uint i = 0, cnt = getEmpireColorCount(); i < cnt; ++i) { + Color col = getEmpireColor(i).color; + bool has = false; + for(uint n = 0, ncnt = empires.length; n < ncnt; ++n) { + if(empires[n] !is setup && empires[n].settings.color.color == col.color) { + has = true; + break; + } + } + if(!has) { + found = true; + setColor = col; + break; + } + } + if(!found) { + Colorf rnd; + rnd.fromHSV(randomd(0, 360.0), randomd(0.5, 1.0), 1.0); + setColor = Color(rnd); + } + setup.settings.color = setColor; + setup.update(); + } + + void tick(double time) { + if(mapIcons.length == 0) { + mapIcons.length = mapCount; + for(uint i = 0, cnt = mapCount; i < cnt; ++i) { + auto@ mp = getMap(i); + if(mp.isListed && !mp.isScenario && mp.icon.length != 0) + mapIcons[i].load(mp.icon); + } + } + inviteButton.visible = cloud::inLobby; + addAIButton.disabled = empires.length >= 28; + if(mpServer) { + bool allReady = true; + for(uint n = 0, ncnt = empires.length; n < ncnt; ++n) { + auto@ emp = empires[n]; + if(emp.playerId != -1 && emp.playerId != CURRENT_PLAYER.id) { + emp.found = false; + if(!emp.settings.ready) + allReady = false; + } + } + + array@ players = getPlayers(); + for(uint i = 0, cnt = players.length; i < cnt; ++i) { + Player@ pl = players[i]; + if(pl == CURRENT_PLAYER) + continue; + + //Find if we already have an empire + bool found = false; + for(uint n = 0, ncnt = empires.length; n < ncnt; ++n) { + auto@ emp = empires[n]; + if(emp.playerId == pl.id) { + emp.found = true; + found = true; + if(pl.name.length != 0 && emp.name.text.length == 0) + emp.name.text = pl.name; + } + } + + if(!found) { + auto@ emp = addEmpire(false, getRacePreset(0)); + emp.name.text = pl.name; + emp.address = pl.address; + emp.setPlayer(pl.id); + } + } + + //Prune disconnected players + for(uint n = 0, ncnt = empires.length; n < ncnt; ++n) { + auto@ emp = empires[n]; + if(emp.playerId != -1 && !emp.found) { + removeEmpire(emp); + --n; --ncnt; + } + } + + //Update play button + if(allReady) + playButton.color = colors::Green; + else + playButton.color = colors::Orange; + } + else if(fromMP) { + if(game_running) { + hideNewGame(true); + switchToMenu(main_menu, snap=true); + return; + } + if(awaitingGalaxy) { + hideNewGame(true); + switchToMenu(main_menu, snap=true); + showMultiplayer(); + return; + } + + auto@ pl = findPlayer(CURRENT_PLAYER.id); + if(pl !is null && pl.settings.ready) { + playButton.text = locale::MP_READY; + playButton.color = colors::Green; + } + else { + playButton.text = locale::MP_NOT_READY; + playButton.color = colors::Orange; + } + + if(!mpIsConnected()) { + message("Lost connection to server:\n " + +localize("DISCONNECT_"+uint(mpDisconnectReason))); + + hideNewGame(true); + switchToMenu(main_menu, snap=true); + showMultiplayer(); + } + } + } + + EmpireSetup@ addEmpire(bool player = false, const RacePreset@ preset = null) { + if(empires.length >= 28) + return null; + empireBG.title = locale::MENU_EMPIRES + " (" + (empires.length + 1) + ")"; + uint y = empires.length * (EMPIRE_SETUP_HEIGHT + 8) + 8; + EmpireSetup@ emp = EmpireSetup(this, + Alignment(Left+4, Top+y, Right-4, Top+y + EMPIRE_SETUP_HEIGHT), + player); + portraits.randomize(emp.settings); + if(player && settings::sNickname.length != 0) + emp.name.text = settings::sNickname; + else + emp.name.text = "Empire "+(nextEmpNum++); + if(preset is null) { + if(player) + @preset = getRacePreset(0); + else + @preset = getUniquePreset(); + } + emp.defaultName = emp.name.text; + emp.update(); + empires.insertLast(emp); + empirePanel.updateAbsolutePosition(); + if(preset !is null) + emp.applyRace(preset); + else if(!player) + emp.resetName(); + if(!player) + setUniqueColor(emp); + return emp; + } + + EmpireSetup@ findPlayer(int id) { + for(uint i = 0, cnt = empires.length; i < cnt; ++i) { + if(empires[i].playerId == id) + return empires[i]; + } + return null; + } + + void clearEmpires() { + for(uint i = 0, cnt = empires.length; i < cnt; ++i) + empires[i].remove(); + empires.length = 0; + nextEmpNum = 2; + updateEmpirePositions(); + } + + void removeEmpire(EmpireSetup@ emp) { + emp.remove(); + empires.remove(emp); + updateEmpirePositions(); + } + + void updateEmpirePositions() { + uint cnt = empires.length; + for(uint i = 0; i < cnt; ++i) { + EmpireSetup@ emp = empires[i]; + emp.alignment.top.pixels = i * (EMPIRE_SETUP_HEIGHT + 8) + 8; + emp.alignment.bottom.pixels = emp.alignment.top.pixels + EMPIRE_SETUP_HEIGHT; + emp.updateAbsolutePosition(); + } + } + + GalaxySetup@ addGalaxy(Map@ mp) { + uint y = galaxies.length * (GALAXY_SETUP_HEIGHT + 8) + 8; + GalaxySetup@ glx = GalaxySetup(this, + Alignment(Left+8, Top+y, Right-8, Top+y + GALAXY_SETUP_HEIGHT), + mp); + + if(mp.eatsPlayers) { + for(uint i = 0, cnt = galaxies.length; i < cnt; ++i) + galaxies[i].setHomeworlds(false); + } + else { + bool haveEating = false; + for(uint i = 0, cnt = galaxies.length; i < cnt; ++i) { + if(galaxies[i].mp.eatsPlayers) { + haveEating = true; + } + } + + if(haveEating) + glx.setHomeworlds(false); + } + + addGalaxyButton.position = vec2i((galaxyPanel.size.width - addGalaxyButton.size.width)/2, y + GALAXY_SETUP_HEIGHT); + galaxies.insertLast(glx); + galaxyPanel.updateAbsolutePosition(); + updateGalaxyPositions(); + return glx; + } + + void removeGalaxy(GalaxySetup@ glx) { + glx.remove(); + galaxies.remove(glx); + updateGalaxyPositions(); + + if(glx.mp.eatsPlayers) { + bool haveEating = false; + for(uint i = 0, cnt = galaxies.length; i < cnt; ++i) { + if(galaxies[i].mp.eatsPlayers) { + haveEating = true; + } + } + + if(!haveEating) { + for(uint i = 0, cnt = galaxies.length; i < cnt; ++i) { + galaxies[i].setHomeworlds(true); + } + } + } + + if(galaxies.length == 0) { + mapHeader.text = locale::CHOOSE_MAP; + mapPanel.visible = true; + galaxyPanel.visible = false; + choosingMap = true; + updateMapList(); + } + } + + void updateGalaxyPositions() { + uint cnt = galaxies.length; + for(uint i = 0; i < cnt; ++i) { + GalaxySetup@ glx = galaxies[i]; + glx.alignment.top.pixels = i * (GALAXY_SETUP_HEIGHT + 8) + 8; + glx.alignment.bottom.pixels = glx.alignment.top.pixels + GALAXY_SETUP_HEIGHT; + glx.updateAbsolutePosition(); + } + addGalaxyButton.position = vec2i((galaxyPanel.size.width - addGalaxyButton.size.width)/2, cnt * (GALAXY_SETUP_HEIGHT + 8) + 6); + galaxyPanel.updateAbsolutePosition(); + } + + void apply() { + apply(settings); + } + + void reset() { + uint newCnt = settings.empires.length; + uint oldCnt = empires.length; + for(uint i = newCnt; i < oldCnt; ++i) { + removeEmpire(empires[i]); + --i; --oldCnt; + } + for(uint i = 0; i < newCnt; ++i) { + EmpireSetup@ setup; + if(i >= oldCnt) + @setup = addEmpire(); + else + @setup = empires[i]; + auto@ sett = settings.empires[i]; + if(setup.playerId == sett.playerId + && setup.playerId == CURRENT_PLAYER.id) { + if(setup.settings.delta > sett.delta) + setup.apply(settings.empires[i]); + else + setup.load(settings.empires[i]); + } + else { + setup.load(settings.empires[i]); + } + } + updateEmpirePositions(); + + newCnt = settings.galaxies.length; + oldCnt = galaxies.length; + for(uint i = newCnt; i < oldCnt; ++i) { + removeGalaxy(galaxies[i]); + --i; --oldCnt; + } + for(uint i = 0; i < newCnt; ++i) { + GalaxySetup@ setup; + if(i >= oldCnt) + @setup = addGalaxy(getMap(settings.galaxies[i].map_id)); + else + @setup = galaxies[i]; + setup.load(settings.galaxies[i]); + } + updateGalaxyPositions(); + + for(uint i = 0, cnt = GAME_SETTINGS_PAGES.length; i < cnt; ++i) + GAME_SETTINGS_PAGES[i].load(settings); + + addGalaxyButton.visible = !mpClient; + addAIButton.visible = !mpClient; + } + + void reset(GameSettings& settings) { + this.settings = settings; + reset(); + } + + void apply(GameSettings& settings) { + uint empCnt = empires.length; + settings.empires.length = empCnt; + for(uint i = 0; i < empCnt; ++i) { + settings.empires[i].index = i; + empires[i].apply(settings.empires[i]); + } + + uint glxCnt = galaxies.length; + settings.galaxies.length = glxCnt; + for(uint i = 0; i < glxCnt; ++i) + galaxies[i].apply(settings.galaxies[i]); + + for(uint i = 0, cnt = GAME_SETTINGS_PAGES.length; i < cnt; ++i) + GAME_SETTINGS_PAGES[i].apply(settings); + } + + void start(){ + apply(); + + Message msg; + settings.write(msg); + + startNewGame(msg); + } + + void switchPage(uint page) { + mapsButton.pressed = page == 0; + galaxyPanel.visible = page == 0 && !choosingMap; + mapPanel.visible = page == 0 && choosingMap; + if(mapPanel.visible) + updateMapList(); + //if(page == 0) + // gameHeader.color = Color(0xff003fff); + resetButton.visible = page != 0 && !mpClient; + + for(uint i = 0, cnt = settingsButtons.length; i < cnt; ++i) { + settingsButtons[i].pressed = page == i+1; + settingsPanels[i].visible = page == i+1; + //if(page == i+1) + // gameHeader.color = GAME_SETTINGS_PAGES[i].color; + } + } + + bool onGuiEvent(const GuiEvent& event) { + switch(event.type) { + case GUI_Clicked: + if(event.caller is playButton) { + if(fromMP) { + auto@ pl = findPlayer(CURRENT_PLAYER.id); + if(pl !is null) { + pl.settings.ready = !pl.settings.ready; + pl.submit(); + } + } + else { + if(mpServer) { + bool allReady = true; + for(uint n = 0, ncnt = empires.length; n < ncnt; ++n) { + auto@ emp = empires[n]; + if(emp.playerId != -1 && emp.playerId != CURRENT_PLAYER.id) { + if(!emp.settings.ready) + allReady = false; + } + } + + if(!allReady) { + question(locale::MP_CONFIRM_NOT_READY, ConfirmStart()); + return true; + } + } + else { + uint sysCount = 0; + apply(); + for(uint i = 0, cnt = settings.galaxies.length; i < cnt; ++i) + sysCount += settings.galaxies[i].systemCount * settings.galaxies[i].galaxyCount; + uint empCount = empires.length; + if(sysCount > REC_MAX_OHGOD) { + question(locale::NG_WARN_OHGOD, ConfirmStart()); + return true; + } + else if(sysCount > REC_MAX_BAD) { + question(locale::NG_WARN_BAD, ConfirmStart()); + return true; + } + else if(sysCount > REC_MAX_OPTIMAL) { + question(locale::NG_WARN_OPTIMAL, ConfirmStart()); + return true; + } + else if(sysCount > REC_MAX_PEREMP * empCount) { + question(locale::NG_WARN_PEREMP, ConfirmStart()); + return true; + } + } + start(); + hideNewGame(true); + } + return true; + } + else if(event.caller is backButton) { + if(!game_running) + mpDisconnect(); + hideNewGame(); + return true; + } + else if(event.caller is inviteButton) { + cloud::inviteFriend(); + return true; + } + else if(event.caller is addAIButton) { + addEmpire(); + return true; + } + else if(event.caller is addGalaxyButton) { + mapHeader.text = locale::ADD_GALAXY; + mapPanel.visible = true; + galaxyPanel.visible = false; + updateMapList(); + choosingMap = true; + return true; + } + else if(event.caller is resetButton) { + for(uint i = 0, cnt = GAME_SETTINGS_PAGES.length; i < cnt; ++i) { + if(settingsPanels[i].visible) + GAME_SETTINGS_PAGES[i].reset(); + } + return true; + } + else if(event.caller is mapsButton) { + switchPage(0); + return true; + } + else { + for(uint i = 0, cnt = settingsButtons.length; i < cnt; ++i) { + if(event.caller is settingsButtons[i]) { + switchPage(i+1); + return true; + } + } + } + break; + case GUI_Confirmed: + if(event.caller is chatBox) { + string message = chatBox.text; + if(message.length != 0) + menuChat(message); + chatBox.text = ""; + } + break; + case GUI_Changed: + if(event.caller is mapList) { + if(mapList.selected != -1) + addGalaxy(cast(mapList.selectedItem).mp); + if(galaxies.length != 0) { + mapList.clearSelection(); + mapPanel.visible = false; + galaxyPanel.visible = true; + choosingMap = false; + } + return true; + } + break; + case GUI_Animation_Complete: + animating = false; + return true; + } + + return BaseGuiElement::onGuiEvent(event); + } + + void updateAbsolutePosition() { + if(!animating) { + if(!hide) { + size = parent.size; + position = vec2i(0, 0); + } + else { + size = parent.size; + position = vec2i(size.x, 0); + } + } + if(fromMP || mpServer) { + chatBG.visible = true; + chatLog.size = vec2i(chatPanel.size.width-20, chatLog.size.height); + empireBG.alignment.bottom.pixels = 262; + } + else { + chatBG.visible = false; + empireBG.alignment.bottom.pixels = 0; + } + addGalaxyButton.position = vec2i((galaxyPanel.size.width - addGalaxyButton.size.width)/2, addGalaxyButton.position.y); + BaseGuiElement::updateAbsolutePosition(); + } + + void animateIn() { + animating = true; + hide = false; + + rect = recti_area(vec2i(parent.size.x, 0), parent.size); + animate_time(this, recti_area(vec2i(), parent.size), MSLIDE_TIME); + } + + void animateOut() { + animating = true; + hide = true; + + rect = recti_area(vec2i(), parent.size); + animate_time(this, recti_area(vec2i(parent.size.x, 0), parent.size), MSLIDE_TIME); + } +}; + +void drawRace(const Skin@ skin, const recti& absPos, const string& name, + const string& portrait, const array@ traits = null, bool showTraits = true) { + const Font@ normal = skin.getFont(FT_Normal); + const Font@ bold = skin.getFont(FT_Bold); + recti namePos = recti_area(absPos.topLeft + vec2i(8, 0), vec2i(absPos.width * 0.35, absPos.height)); + + //Portrait + auto@ prt = getEmpirePortrait(portrait); + if(prt !is null) { + prt.portrait.draw(recti_area(absPos.topLeft + vec2i(8, 0), vec2i(absPos.height, absPos.height))); + namePos.topLeft.x += absPos.height+8; + } + + //Race name + bold.draw(pos=namePos, text=name); + + //FTL Method + recti ftlPos = recti_area(absPos.topLeft + vec2i(absPos.width*0.35 + 16, 0), + vec2i(absPos.width * 0.35, absPos.height)); + + //Traits + if(traits !is null) { + recti pos = recti_area(vec2i(absPos.botRight.x - 32, absPos.topLeft.y + 3), vec2i(24, 24)); + for(uint i = 0, cnt = traits.length; i < cnt; ++i) { + auto@ trait = traits[i]; + if(trait.unique == "FTL") { + trait.icon.draw(recti_area(ftlPos.topLeft, vec2i(absPos.height, absPos.height)).aspectAligned(trait.icon.aspect)); + ftlPos.topLeft.x += absPos.height+8; + normal.draw(text=trait.name, pos=ftlPos); + } + else if(showTraits) { + traits[i].icon.draw(pos.aspectAligned(traits[i].icon.aspect)); + pos -= vec2i(24, 0); + } + } + } +} + +Color colorFromNumber(int num) { + float hue = (num*26534371)%360; + Colorf col; + col.fromHSV(hue, 1.f, 1.f); + return Color(col); +} + +class RaceElement : GuiListElement { + const RacePreset@ preset; + + RaceElement(const RacePreset@ preset) { + @this.preset = preset; + } + + void draw(GuiListbox@ ele, uint flags, const recti& absPos) { + drawRace(ele.skin, absPos, preset.name, preset.portrait, preset.traits); + } +}; + +class CustomRaceElement : GuiListElement { + const EmpireSettings@ settings; + + CustomRaceElement(const EmpireSettings@ settings) { + @this.settings = settings; + } + + void draw(GuiListbox@ ele, uint flags, const recti& absPos) { + drawRace(ele.skin, absPos, settings.raceName, settings.portrait, settings.traits); + } +}; + +class CurrentRaceElement : GuiListElement { + EmpireSettings@ settings; + bool valid = true; + + CurrentRaceElement(EmpireSettings@ settings) { + @this.settings = settings; + } + + void update() { + valid = settings.getTraitPoints() >= 0 && !settings.hasTraitConflicts(); + } + + void draw(GuiListbox@ ele, uint flags, const recti& absPos) { + if(!valid) { + Color color(0xff0000ff); + color.a = abs((frameTime % 1.0) - 0.5) * 2.0 * 255.0; + ele.skin.draw(SS_Button, SF_Normal, absPos.padded(-5, -3), color); + } + drawRace(ele.skin, absPos, settings.raceName, settings.portrait, traits=settings.traits, showTraits=false); + } +}; + +class CustomizeOption : GuiListElement { + void draw(GuiListbox@ ele, uint flags, const recti& absPos) { + const Font@ bold = ele.skin.getFont(FT_Bold); + + recti namePos = recti_area(absPos.topLeft + vec2i(8, 0), vec2i(absPos.width * 0.95, absPos.height)); + icons::Customize.draw(recti_area(absPos.topLeft + vec2i(8, 0), vec2i(absPos.height, absPos.height))); + namePos.topLeft.x += absPos.height+8; + + bold.draw(pos=namePos, text=locale::CUSTOMIZE_RACE, color=Color(0xff8000ff)); + } +}; + +class TraitList : GuiIconGrid { + array traits; + + TraitList(IGuiElement@ parent, Alignment@ align) { + super(parent, align); + + MarkupTooltip tt(350, 0.f, true, true); + tt.Lazy = true; + tt.LazyUpdate = false; + tt.Padding = 4; + @tooltipObject = tt; + } + + uint get_length() override { + return traits.length; + } + + string get_tooltip() override { + if(hovered < 0 || hovered >= int(length)) + return ""; + + auto@ trait = traits[hovered]; + return format("[color=$1][b]$2[/b][/color]\n$3", + toString(trait.color), trait.name, trait.description); + } + + void drawElement(uint i, const recti& pos) override { + traits[i].icon.draw(pos.aspectAligned(traits[i].icon.aspect)); + } +}; + +class ChangeWelfare : GuiContextOption { + ChangeWelfare(const string& text, uint index) { + value = int(index); + this.text = text; + icon = Sprite(spritesheet::ConvertIcon, index); + } + + void call(GuiContextMenu@ menu) override { + playerEmpire.WelfareMode = uint(value); + } +}; + +const Sprite[] DIFF_SPRITES = { + Sprite(material::HappyFace), + Sprite(material::StatusPeace), + Sprite(material::StatusWar), + Sprite(material::StatusCeaseFire), + Sprite(spritesheet::AttributeIcons, 3), + Sprite(spritesheet::AttributeIcons, 0), + Sprite(spritesheet::VoteIcons, 3), + Sprite(spritesheet::VoteIcons, 3, colors::Red) +}; + +const Color[] DIFF_COLORS = { + colors::Green, + colors::White, + colors::White, + colors::White, + colors::Orange, + colors::Red, + colors::Red, + colors::Red +}; + +const string[] DIFF_TOOLTIPS = { + locale::DIFF_PASSIVE, + locale::DIFF_EASY, + locale::DIFF_NORMAL, + locale::DIFF_HARD, + locale::DIFF_MURDEROUS, + locale::DIFF_CHEATING, + locale::DIFF_SAVAGE, + locale::DIFF_BARBARIC, +}; + +class ChangeDifficulty : GuiMarkupContextOption { + int level; + EmpireSetup@ setup; + + ChangeDifficulty(EmpireSetup@ setup, int value, const string& text) { + level = value; + set(text); + @this.setup = setup; + } + + void call(GuiContextMenu@ menu) override { + setup.settings.difficulty = level; + setup.update(); + } +}; + +class ChangeTeam : GuiMarkupContextOption { + int team; + EmpireSetup@ setup; + + ChangeTeam(EmpireSetup@ setup, int value) { + team = value; + if(value >= 0) + set(format("[b][color=$2]$1[/color][/b]", format(locale::TEAM_TEXT, toString(value)), + toString(colorFromNumber(value)))); + else + set(format("[b][color=#aaa]$1...[/color][/b]", locale::NO_TEAM)); + @this.setup = setup; + } + + void call(GuiContextMenu@ menu) override { + setup.settings.team = team; + setup.submit(); + } +}; + +class Chooser : GuiIconGrid { + Color spriteColor; + array colors; + array sprites; + + uint selected = 0; + + Chooser(IGuiElement@ parent, Alignment@ align, const vec2i& itemSize) { + super(parent, align); + horizAlign = 0.5; + vertAlign = 0.0; + iconSize = itemSize; + updateAbsolutePosition(); + } + + void add(const Color& col) { + colors.insertLast(col); + } + + void add(const Sprite& sprt) { + sprites.insertLast(sprt); + } + + uint get_length() override { + return max(colors.length, sprites.length); + } + + void drawElement(uint index, const recti& pos) override { + if(uint(selected) == index) + drawRectangle(pos, Color(0xffffff30)); + if(uint(hovered) == index) + drawRectangle(pos, Color(0xffffff30)); + if(index < colors.length) + drawRectangle(pos.padded(5), colors[index]); + if(index < sprites.length) + sprites[index].draw(pos, spriteColor); + } +}; + +class RaceChooser : GuiOverlay { + EmpireSetup@ setup; + GuiSkinElement@ panel; + + GuiText@ header; + GuiPanel@ list; + + const RacePreset@ selectedRace; + array presetButtons; + array racePresets; + + GuiSprite@ portrait; + GuiSprite@ flag; + GuiSprite@ bgDisplay; + + GuiPanel@ descScroll; + GuiMarkupText@ description; + + GuiPanel@ loreScroll; + GuiMarkupText@ lore; + + GuiButton@ playButton; + GuiButton@ customizeButton; + GuiButton@ randomizeButton; + GuiButton@ loadButton; + GuiButton@ backButton; + bool isInitial; + bool hasChosenRace = false; + bool chosenShipset = false; + + Chooser@ flags; + Chooser@ colors; + ShipsetChooser@ shipsets; + + RaceChooser(EmpireSetup@ setup, bool isInitial = false) { + @this.setup = setup; + this.isInitial = isInitial; + super(null); + closeSelf = false; + + @panel = GuiSkinElement(this, Alignment(Left-4, Top+0.05f, Right+4, Bottom-0.05f), SS_Panel); + + @customizeButton = GuiButton(panel, Alignment(Right-232, Bottom-78, Width=220, Height=33)); + customizeButton.text = locale::CUSTOMIZE_RACE; + customizeButton.setIcon(icons::Edit); + + @randomizeButton = GuiButton(panel, Alignment(Right-452, Bottom-78, Width=220, Height=33)); + randomizeButton.text = "Randomize Race"; + + @loadButton = GuiButton(panel, Alignment(Right-232, Bottom-78+33, Width=220, Height=33)); + loadButton.text = locale::LOAD_CUSTOM_RACE; + loadButton.setIcon(icons::Load); + + int w = 250, h = 140; + int off = max((size.width - (getRacePresetCount() * w)) / 2 - 20, 0); + + GuiSkinElement listBG(panel, Alignment(Left-4, Top+12, Right+4, Top+154), SS_PlainBox); + + @list = GuiPanel(panel, Alignment(Left+off, Top+12, Right-off, Top+174)); + updateAbsolutePosition(); + + vec2i pos; + uint curSelection = 0; + for(uint i = 0, cnt = getRacePresetCount(); i < cnt; ++i) { + auto@ preset = getRacePreset(i); + if(preset.dlc.length != 0 && !hasDLC(preset.dlc)) + continue; + + racePresets.insertLast(preset); + + GuiButton btn(list, recti_area(pos.x, pos.y, w, h)); + btn.toggleButton = true; + btn.style = SS_GlowButton; + btn.pressed = i == 0; + + GuiSprite icon(btn, recti_area(2, 2, w*0.75, h-4)); + icon.horizAlign = 0.0; + icon.vertAlign = 1.0; + icon.desc = getSprite(preset.portrait); + + GuiText name(btn, recti_area(0, 0, w-4, h)); + name.font = FT_Big; + name.stroke = colors::Black; + name.text = preset.name; + name.vertAlign = 0.4; + name.horizAlign = 0.9; + + GuiSkinElement tagbar(btn, recti_area(1, h-28, w-3, 24), SS_PlainBox); + tagbar.color = Color(0xffffff80); + + GuiText tagline(btn, recti_area(0, h-30, w-4, 24)); + tagline.font = FT_Italic; + tagline.stroke = colors::Black; + tagline.color = Color(0xaaaaaaff); + tagline.text = preset.tagline; + tagline.horizAlign = 1.0; + + TraitList traits(btn, Alignment(Left, Bottom-56, Right, Bottom-28)); + traits.iconSize = vec2i(24, 24); + traits.horizAlign = 1.0; + traits.fallThrough = true; + traits.traits = preset.traits; + + if(preset.equals(setup.settings)) { + curSelection = i; + hasChosenRace = true; + } + + if(!setup.player && !preset.aiSupport) { + icon.saturation = 0.f; + traits.visible = false; + btn.disabled = true; + btn.color = Color(0xffffffaa); + name.color = Color(0xaa3030ff); + + setMarkupTooltip(btn, locale::AI_CANNOT_PLAY); + } + + presetButtons.insertLast(btn); + pos.x += w; + } + + + BaseGuiElement leftBG(panel, Alignment(Left+12, Top+174, Left+0.33f-6, Bottom-90)); + int y = 0; + + GuiSkinElement portBG(leftBG, Alignment(Left, Top+y, Right, Bottom), SS_PlainBox); + + @bgDisplay = GuiSprite(portBG, Alignment().padded(2), Sprite(getEmpireColor(setup.settings.color).background)); + bgDisplay.color = Color(0xffffff80); + bgDisplay.stretchOutside = true; + + @portrait = GuiSprite(portBG, Alignment(Left+2, Top, Right-2, Height=232)); + portrait.horizAlign = 0.0; + portrait.vertAlign = 1.0; + + @flag = GuiSprite(portBG, Alignment(Right-164, Top+4, Width=160, Height=160)); + flag.horizAlign = 1.0; + flag.vertAlign = 0.0; + flag.color = setup.settings.color; + flag.color.a = 0xc0; + flag.desc = getSprite(setup.settings.flag); + + y += 220 + 12; + GuiSkinElement colBG(leftBG, Alignment(Left, Top+y, Right, Height=34), SS_PlainBox); + @colors = Chooser(colBG, Alignment().padded(8, 0), vec2i(48, 32)); + for(uint i = 0, cnt = getEmpireColorCount(); i < cnt; ++i) { + Color color = getEmpireColor(i).color; + colors.add(color); + if(color.color == setup.settings.color.color) + colors.selected = i; + } + updateAbsolutePosition(); + + y += 34 + 12; + GuiSkinElement flagBG(leftBG, Alignment(Left, Top+y, Right, Height=110), SS_PlainBox); + @flags = Chooser(flagBG, Alignment().padded(8, 0), vec2i(48, 48)); + flags.spriteColor = setup.settings.color; + for(uint i = 0, cnt = getEmpireFlagCount(); i < cnt; ++i) { + string flag = getSpriteDesc(Sprite(getEmpireFlag(i).flag)); + flags.add(getSprite(flag)); + if(flag == setup.settings.flag) + flags.selected = i; + } + + y += 110 + 12; + // DOF - Adjust shipset selection box size. + // Trying dynamic size based on number of shipsets. Target 6 per row, max 4 rows (don't want to get too tall for lower resolutions). + uint SSBoxHeight = min(int(ceil(getShipsetCount()/8.0)),4) * 75; + GuiSkinElement shipsetBG(leftBG, Alignment(Left, Top+y, Right, Height=SSBoxHeight), SS_PlainBox); + @shipsets = ShipsetChooser(shipsetBG, Alignment().padded(8, 0), vec2i(160, 70)); + shipsets.selectedColor = setup.settings.color; + shipsets.selected = 0; + shipsets.horizAlign = 0.0; + for(uint i = 0, cnt = getShipsetCount(); i < cnt; ++i) { + auto@ ss = getShipset(i); + if(ss.available && (ss.dlc.length == 0 || hasDLC(ss.dlc))) + shipsets.add(ss); + if(ss.ident == setup.settings.shipset) + shipsets.selected = shipsets.length-1; + } + + GuiSkinElement loreBox(panel, Alignment(Left+0.33f+6, Top+174, Left+0.66f-6, Bottom-90), SS_PlainBox); + @loreScroll = GuiPanel(loreBox, Alignment().fill()); + @lore = GuiMarkupText(loreScroll, recti_area(12, 12, 376, 100)); + lore.fitWidth = true; + + GuiSkinElement descBox(panel, Alignment(Left+0.66f+6, Top+174, Right-12, Bottom-90), SS_PlainBox); + @descScroll = GuiPanel(descBox, Alignment().fill()); + @description = GuiMarkupText(descScroll, recti_area(12, 12, 376, 100)); + description.fitWidth = true; + + @playButton = GuiButton(panel, Alignment(Left+0.5f-150, Bottom-78, Left+0.5f+150, Bottom-12)); + playButton.font = FT_Medium; + playButton.color = Color(0x00c0ffff); + + @backButton = GuiButton(panel, Alignment(Left+12, Bottom-78, Left+220, Bottom-12), locale::BACK); + backButton.font = FT_Medium; + backButton.buttonIcon = icons::Back; + + selectRace(curSelection); + updateAbsolutePosition(); + updateAbsolutePosition(); + } + + void updateAbsolutePosition() { + if(shipsets !is null && shipsets.parent !is null) + shipsets.parent.visible = screenSize.height >= 900; + BaseGuiElement::updateAbsolutePosition(); + } + + void close() override { + if(isInitial) + return; + GuiOverlay::close(); + } + + void selectRace(uint select) { + for(uint i = 0, cnt = presetButtons.length; i < cnt; ++i) + presetButtons[i].pressed = i == select; + + auto@ preset = racePresets[select]; + + string desc; + if(preset.isHard) + desc += format("[font=Subtitle][color=#ffc000]$1[/color][/font]", locale::RACE_IS_HARD); + for(uint i = 0, cnt = preset.traits.length; i < cnt; ++i) { + if(desc.length != 0) + desc += "\n\n"; + desc += format("[font=Medium]$1[/font][vspace=4/]\n[offset=20]", preset.traits[i].name); + desc += preset.traits[i].description; + desc += "[/offset]"; + } + description.text = desc; + + string txt = format("[font=Big]$1[/font]\n", preset.name); + txt += format("[right][font=Medium][color=#aaa]$1[/color][/font][/right]\n\n", preset.tagline); + txt += preset.lore; + lore.text = txt; + + if(isInitial) + playButton.text = format(locale::PLAY_AS_RACE, preset.name); + else + playButton.text = format(locale::CHOOSE_A_RACE, preset.name); + playButton.buttonIcon = getSprite(preset.portrait); + + portrait.desc = getSprite(preset.portrait); + + loreScroll.updateAbsolutePosition(); + descScroll.updateAbsolutePosition(); + @selectedRace = preset; + + if(!chosenShipset) { + setup.settings.shipset = preset.shipset; + for(uint i = 0, cnt = shipsets.length; i < cnt; ++i) { + if(shipsets.items[i].ident == preset.shipset) { + shipsets.selected = i; + break; + } + } + } + } + + bool onGuiEvent(const GuiEvent& evt) { + if(evt.type == GUI_Clicked) { + if(evt.caller is flags) { + uint sel = flags.hovered; + if(sel != uint(-1)) { + string sprt = getSpriteDesc(Sprite(getEmpireFlag(sel).flag)); + setup.settings.flag = sprt; + flag.desc = getSprite(sprt); + flags.selected = sel; + } + return true; + } + else if(evt.caller is colors) { + uint sel = colors.hovered; + if(sel != uint(-1)) { + auto empCol = getEmpireColor(sel); + Color col = empCol.color; + setup.settings.color = col; + bgDisplay.desc = Sprite(empCol.background); + flag.color = col; + flag.color.a = 0xc0; + flags.spriteColor = col; + shipsets.selectedColor = col; + colors.selected = sel; + } + return true; + } + else if(evt.caller is shipsets) { + uint sel = shipsets.hovered; + if(sel != uint(-1)) { + chosenShipset = true; + setup.settings.shipset = shipsets.items[sel].ident; + shipsets.selected = sel; + } + return true; + } + else if(evt.caller is customizeButton) { + isInitial = false; + if(hasChosenRace) { + setup.applyRace(selectedRace); + setup.submit(); + } + setup.openRaceWindow(); + close(); + return true; + } + else if(evt.caller is backButton) { + if(isInitial) { + isInitial = false; + new_game.backButton.emitClicked(); + close(); + return true; + } + close(); + return true; + } + else if(evt.caller is randomizeButton) { + hasChosenRace = true; + selectRace(randomi(0, presetButtons.length - 1)); + } + else if(evt.caller is loadButton) { + isInitial = false; + LoadRaceDialog(null, setup.settings, setup); + close(); + return true; + } + else if(evt.caller is playButton) { + setup.applyRace(selectedRace); + setup.submit(); + if(isInitial) { + isInitial = false; + setup.resetName(); + setup.ng.resetAIColors(); + setup.ng.resetAIRaces(); + } + close(); + return true; + } + else { + for(uint i = 0, cnt = presetButtons.length; i < cnt; ++i) { + if(evt.caller.isChildOf(presetButtons[i])) { + hasChosenRace = true; + selectRace(i); + return true; + } + } + } + } + return GuiOverlay::onGuiEvent(evt); + } +}; + +class EmpireSetup : BaseGuiElement, IGuiCallback { + GuiButton@ portraitButton; + GuiEmpire@ portrait; + EmpireSettings settings; + + NewGame@ ng; + GuiTextbox@ name; + GuiButton@ removeButton; + GuiText@ handicapLabel; + GuiSpinbox@ handicap; + GuiButton@ raceBox; + GameAddress address; + GuiButton@ colorButton; + GuiButton@ flagButton; + GuiButton@ difficulty; + GuiButton@ aiSettings; + GuiSprite@ aiIcon; + GuiText@ aiText; + GuiButton@ teamButton; + GuiText@ raceName; + GuiSprite@ raceFTLIcon; + GuiText@ raceFTL; + TraitList@ traitList; + bool player; + bool found = true; + int playerId = -1; + ChoosePopup@ popup; + GuiSprite@ readyness; + string defaultName; + + EmpireSetup(NewGame@ menu, Alignment@ align, bool Player = false) { + super(menu.empirePanel, align); + + @portraitButton = GuiButton(this, Alignment(Left+8, Top+4, Left+EMPIRE_SETUP_HEIGHT, Bottom-4)); + portraitButton.style = SS_NULL; + @portrait = GuiEmpire(portraitButton, Alignment().fill()); + @portrait.settings = settings; + + @ng = menu; + @name = GuiTextbox(this, Alignment(Left+EMPIRE_SETUP_HEIGHT+8, Top+14, Right-310, Top+0.5f-4)); + name.font = FT_Subtitle; + name.style = SS_HoverTextbox; + name.selectionColor = Color(0xffffff40); + + @colorButton = GuiButton(this, Alignment(Right-302, Top+14, Width=50, Height=30)); + colorButton.style = SS_HoverButton; + + @flagButton = GuiButton(this, Alignment(Right-244, Top+14, Width=50, Height=30)); + flagButton.style = SS_HoverButton; + + @teamButton = GuiButton(this, Alignment(Right-186, Top+14, Width=50, Height=30)); + teamButton.style = SS_HoverButton; + + @difficulty = GuiButton(this, Alignment(Right-128, Top+14, Width=50, Height=30)); + difficulty.style = SS_HoverButton; + difficulty.visible = false; + + @aiSettings = GuiButton(this, Alignment(Right-128, Top+10, Width=66, Height=38)); + aiSettings.style = SS_HoverButton; + aiSettings.visible = false; + + @aiIcon = GuiSprite(aiSettings, Alignment().padded(1, 1, 1, 5)); + @aiText = GuiText(aiSettings, Alignment()); + aiText.horizAlign = 0.5; + aiText.vertAlign = 0.2; + aiText.font = FT_Small; + aiText.stroke = colors::Black; + + @raceBox = GuiButton(this, Alignment(Left+EMPIRE_SETUP_HEIGHT+8, Top+0.5f+4, Right-8, Bottom-14)); + raceBox.style = SS_HoverButton; + + @raceName = GuiText(raceBox, Alignment(Left+8, Top, Left+0.35f, Bottom)); + raceName.font = FT_Bold; + + @raceFTLIcon = GuiSprite(raceBox, Alignment(Left+0.4f, Top, Left+0.4f+22, Bottom)); + @raceFTL = GuiText(raceBox, Alignment(Left+0.4f+26, Top, Right-0.3f, Bottom)); + + @traitList = TraitList(raceBox, Alignment(Right-0.3f, Top+2, Right-30, Bottom)); + traitList.iconSize = vec2i(24, 24); + traitList.horizAlign = 1.0; + traitList.fallThrough = true; + + player = Player; + @removeButton = GuiButton(this, + Alignment(Right-50, Top, Right, Top+30)); + removeButton.color = colors::Red; + removeButton.setIcon(icons::Remove); +// if(!player) { + removeButton.visible = true; + aiSettings.visible = true; +// } +// else { +// removeButton.visible = false; +// playerId = 1; +// } + + @readyness = GuiSprite(portrait, Alignment(Right-40, Bottom-40, Right, Bottom)); + readyness.visible = false; + + applyRace(getRacePreset(randomi(0, getRacePresetCount()-1))); + updateAbsolutePosition(); + } + + void showDifficulties() { + GuiContextMenu menu(mousePos); + menu.itemHeight = 54; + menu.addOption(ChangeDifficulty(this, 0, locale::DIFF_PASSIVE)); + menu.addOption(ChangeDifficulty(this, 1, locale::DIFF_EASY)); + menu.addOption(ChangeDifficulty(this, 2, locale::DIFF_NORMAL)); + menu.addOption(ChangeDifficulty(this, 3, locale::DIFF_HARD)); + menu.addOption(ChangeDifficulty(this, 4, locale::DIFF_MURDEROUS)); + menu.addOption(ChangeDifficulty(this, 5, locale::DIFF_CHEATING)); + menu.addOption(ChangeDifficulty(this, 6, locale::DIFF_SAVAGE)); + menu.addOption(ChangeDifficulty(this, 7, locale::DIFF_BARBARIC)); + + menu.updateAbsolutePosition(); + } + + void showAISettings() { + AIPopup popup(aiSettings, this); + aiSettings.Hovered = false; + aiSettings.Pressed = false; + } + + void showTeams() { + GuiContextMenu menu(mousePos); + menu.itemHeight = 30; + + //Figure out how many distinct teams we have + uint distinctTeams = 0; + uint teamMask = 0; + int maxTeam = 0; + for(uint i = 0, cnt = ng.empires.length; i < cnt; ++i) { + int team = ng.empires[i].settings.team; + if(team < 0) + continue; + + maxTeam = max(maxTeam, team); + uint mask = 1<<(team-1); + if(mask & teamMask == 0) { + teamMask |= mask; + ++distinctTeams; + } + } + + //Add more teams than we currently have + menu.addOption(ChangeTeam(this, -1)); + for(uint i = 1; i <= min(max(distinctTeams+5, maxTeam+1), 30); ++i) + menu.addOption(ChangeTeam(this, i)); + + menu.updateAbsolutePosition(); + } + + void forceAITraits(EmpireSettings& settings) { + for(uint i = 0, cnt = settings.traits.length; i < cnt; ++i) { + auto@ trait = settings.traits[i]; + if(!trait.aiSupport) { + if(trait.unique.length == 0) { + settings.traits.removeAt(i); + --cnt; --i; + } + else { + const Trait@ repl; + uint replCount = 0; + for(uint n = 0, ncnt = getTraitCount(); n < ncnt; ++n) { + auto@ other = getTrait(n); + if(other.unique == trait.unique && other.aiSupport && other.hasDLC) { + replCount += 1; + if(randomd() < 1.0 / double(replCount)) + @repl = other; + } + } + + if(repl !is null) { + @settings.traits[i] = repl; + } + else { + settings.traits.removeAt(i); + --cnt; --i; + } + } + } + } + } + + void applyRace(const RacePreset@ preset) { + preset.apply(settings); + forceAITraits(settings); + if(!player) { + //forceAITraits(settings); + if(defaultName == name.text) + resetName(); + } + update(); + } + + void applyRace(const EmpireSettings@ custom) { + settings.copyRaceFrom(custom); + forceAITraits(settings); + if(!player) { + //settings.copyRaceFrom(custom); + if(defaultName == name.text) + resetName(); + } + } + + void resetName() { + string race = settings.raceName; + if(race.startswith_nocase("the ")) + race = race.substr(4); + name.text = format(localize(empireNames.generate()), race); + defaultName = name.text; + } + + void setPlayer(int id) { + player = id != -1; + playerId = id; + name.disabled = player || !mpClient; + removeButton.visible = !mpClient && (!player || id != CURRENT_PLAYER.id); + aiSettings.visible = !player; + readyness.visible = player && id != 1; + + bool editable = id == CURRENT_PLAYER.id || (!mpClient && id == -1); + raceBox.disabled = !editable; + colorButton.disabled = !editable; + flagButton.disabled = !editable; + teamButton.disabled = !editable; + aiSettings.disabled = !editable; + } + + void openRaceWindow() { + TraitsWindow win(this); + } + + void update() { + updateTraits(); + + if(difficulty.visible) { + difficulty.color = DIFF_COLORS[settings.difficulty]; + setMarkupTooltip(difficulty, locale::TT_DIFF+"\n"+DIFF_TOOLTIPS[settings.difficulty], width=300); + if(difficulty.color.color != colors::White.color) + difficulty.style = SS_Button; + else + difficulty.style = SS_HoverButton; + } + + if(aiSettings.visible) { + aiIcon.desc = QDIFF_ICONS[clamp(settings.difficulty, 0, 2)]; + aiText.color = QDIFF_COLORS[clamp(settings.difficulty, 0, 2)]; + aiText.text = getAIName(settings); + } + + name.textColor = settings.color; + raceName.text = settings.raceName; + for(uint i = 0, cnt = settings.traits.length; i < cnt; ++i) { + auto@ trait = settings.traits[i]; + if(trait.unique == "FTL") { + raceFTLIcon.desc = trait.icon; + raceFTL.text = trait.name; + } + } + } + + void updateTraits() { + traitList.traits.length = 0; + for(uint i = 0, cnt = getTraitCount(); i < cnt; ++i) { + auto@ trait = getTrait(i); + if(settings.hasTrait(trait) && trait.unique != "FTL") + traitList.traits.insertLast(trait); + } + if(settings.ready) { + readyness.tooltip = locale::MP_PLAYER_READY; + readyness.desc = icons::Ready; + } + else { + readyness.tooltip = locale::MP_PLAYER_NOT_READY; + readyness.desc = icons::NotReady; + } + } + + void submit() { + if(mpClient) + changeEmpireSettings(settings); + //if(!player) + forceAITraits(settings); + settings.delta += 1; + update(); + } + + bool onGuiEvent(const GuiEvent& evt) { + switch(evt.type) { + case GUI_Clicked: + if(evt.caller is removeButton) { + if(player) + mpKick(playerId); + else + ng.removeEmpire(this); + return true; + } + else if(evt.caller is difficulty) { + showDifficulties(); + return true; + } + else if(evt.caller is aiSettings) { + showAISettings(); + return true; + } + else if(evt.caller is teamButton) { + showTeams(); + return true; + } + else if(evt.caller is raceBox || evt.caller is portraitButton) { + RaceChooser(this); + raceBox.Hovered = false; + return true; + } + else if(evt.caller is colorButton) { + vec2i pos(evt.caller.absolutePosition.topLeft.x, + evt.caller.absolutePosition.botRight.y); + uint cnt = getEmpireColorCount(); + vec2i size(220, ceil(double(cnt)/4.0) * 38.0); + @popup = ChoosePopup(pos, size, vec2i(48, 32)); + @popup.callback = this; + popup.extraHeight = 60; + ColorPicker picker(popup.overlay, recti_area(pos+vec2i(20,size.y+2), vec2i(size.x-40, 50))); + @picker.callback = this; + for(uint i = 0; i < cnt; ++i) + popup.add(getEmpireColor(i).color); + return true; + } + else if(evt.caller is flagButton) { + vec2i pos(evt.caller.absolutePosition.topLeft.x, + evt.caller.absolutePosition.botRight.y); + uint cnt = getEmpireFlagCount(); + vec2i size(220, ceil(double(cnt)/4.0) * 52.0); + @popup = ChoosePopup(pos, size, vec2i(48, 48)); + @popup.callback = this; + popup.spriteColor = settings.color; + for(uint i = 0; i < cnt; ++i) + popup.add(Sprite(getEmpireFlag(i).flag)); + return true; + } + else if(evt.caller is popup) { + + } + break; + case GUI_Confirmed: + if(evt.caller is popup) { + if(popup.colors.length > 0) + settings.color = getEmpireColor(evt.value).color; + else if(popup.sprites.length > 0) + settings.flag = getEmpireFlag(evt.value).flagDef; + @popup = null; + submit(); + return true; + } + if(cast(evt.caller) !is null) { + settings.color = cast(evt.caller).picked; + popup.remove(); + @popup = null; + submit(); + } + break; + } + return BaseGuiElement::onGuiEvent(evt); + } + + void apply(EmpireSettings& es) { + es = settings; + es.name = name.text; + es.playerId = playerId; + + //if(player || playerId != -1) + // es.type = ET_Player; + //else if(es.type == ET_Player) + es.type = ET_WeaselAI; + } + + void load(EmpireSettings& es) { + name.text = es.name; + player = es.type == uint(ET_Player); + setPlayer(es.playerId); + settings = es; + update(); + } + + void draw() { + Color color = settings.color; + + skin.draw(SS_EmpireSetupItem, SF_Normal, AbsolutePosition.padded(-10, 0), color); + BaseGuiElement::draw(); + + if(colorButton.visible) { + setClip(colorButton.absoluteClipRect); + drawRectangle(colorButton.absolutePosition.padded(6), color); + } + + auto@ flag = getEmpireFlag(settings.flag); + if(flag !is null && flagButton.visible) { + setClip(flagButton.absoluteClipRect); + flag.flag.draw(recti_centered(flagButton.absolutePosition, + vec2i(flagButton.size.height, flagButton.size.height)), + color); + } + + if(difficulty.visible) { + setClip(difficulty.absoluteClipRect); + DIFF_SPRITES[settings.difficulty].draw(recti_centered(difficulty.absolutePosition, + vec2i(difficulty.size.height, difficulty.size.height))); + } + + if(teamButton.visible) { + setClip(teamButton.absoluteClipRect); + if(settings.team >= 0) { + material::TabDiplomacy.draw(recti_centered(teamButton.absolutePosition, + vec2i(teamButton.size.height, teamButton.size.height))); + skin.getFont(FT_Small).draw( + pos=teamButton.absolutePosition, + text=locale::TEAM, + horizAlign=0.5, vertAlign=0.0, + stroke=colors::Black, + color=colors::White); + skin.getFont(FT_Medium).draw( + pos=teamButton.absolutePosition, + text=toString(settings.team), + horizAlign=0.5, vertAlign=1.0, + stroke=colors::Black, + color=colorFromNumber(settings.team)); + } + else { + shader::SATURATION_LEVEL = 0.f; + material::TabDiplomacy.draw(recti_centered(teamButton.absolutePosition, + vec2i(teamButton.size.height, teamButton.size.height)), + Color(0xffffff80), shader::Desaturate); + } + } + } +}; + +string getAIName(EmpireSettings& settings) { + string text; + text = QDIFF_NAMES[clamp(settings.difficulty, 0, 2)]; + + if(settings.aiFlags & AIF_Passive != 0) + text += "|"; + if(settings.aiFlags & AIF_Aggressive != 0) + text += "@"; + if(settings.aiFlags & AIF_Biased != 0) + text += "^"; + if(settings.aiFlags & AIF_CheatPrivileged != 0) + text += "$"; + if(settings.type == ET_BumAI) + text += "?"; + + int cheatLevel = 0; + if(settings.cheatWealth > 0) + cheatLevel += ceil(double(settings.cheatWealth) / 10.0); + if(settings.cheatStrength > 0) + cheatLevel += settings.cheatStrength; + if(settings.cheatAbundance > 0) + cheatLevel += settings.cheatAbundance; + + if(cheatLevel > 0) { + if(cheatLevel > 3) + cheatLevel = 3; + while(cheatLevel > 0) { + text += "+"; + cheatLevel -= 1; + } + } + + return text; +} + +class AIPopup : BaseGuiElement { + GuiOverlay@ overlay; + + GuiListbox@ difficulties; + + GuiText@ behaveHeading; + GuiText@ cheatHeading; + + EmpireSetup@ setup; + + GuiCheckbox@ aggressive; + GuiCheckbox@ passive; + GuiCheckbox@ biased; + GuiCheckbox@ legacy; + + GuiCheckbox@ wealth; + GuiSpinbox@ wealthAmt; + GuiCheckbox@ strength; + GuiSpinbox@ strengthAmt; + GuiCheckbox@ abundance; + GuiSpinbox@ abundanceAmt; + GuiCheckbox@ privileged; + + GuiButton@ okButton; + GuiButton@ applyToAllButton; + + AIPopup(IGuiElement@ around, EmpireSetup@ setup) { + @overlay = GuiOverlay(null); + overlay.closeSelf = false; + overlay.fade.a = 0; + @this.setup = setup; + + recti pos = recti_area( + vec2i(around.absolutePosition.botRight.x, around.absolutePosition.topLeft.y), + vec2i(600, 200)); + if(pos.botRight.y > screenSize.y) + pos += vec2i(0, screenSize.y - pos.botRight.y); + if(pos.botRight.x > screenSize.x) + pos += vec2i(screenSize.x - pos.botRight.x, 0); + + super(overlay, pos); + updateAbsolutePosition(); + setGuiFocus(this); + + @difficulties = GuiListbox(this, Alignment(Left+4, Top+4, Left+250, Bottom-4)); + difficulties.required = true; + difficulties.itemHeight = 64; + + for(uint i = 0; i < 3; ++i) { + difficulties.addItem(GuiMarkupListText( + format("[color=$1][font=Medium][stroke=#000]$2[/stroke][/font][/color]\n[color=#aaa][i]$3[/i][/color]", + toString(QDIFF_COLORS[i]), QDIFF_NAMES[i], QDIFF_DESC[i]))); + } + + @behaveHeading = GuiText(this, Alignment(Left+260, Top+6, Left+260+170, Top+36)); + behaveHeading.font = FT_Medium; + behaveHeading.stroke = colors::Black; + behaveHeading.text = locale::AI_BEHAVIOR; + + pos = recti_area(vec2i(260, 36), vec2i(170, 30)); + + @aggressive = GuiCheckbox(this, pos, locale::AI_AGGRESSIVE); + setMarkupTooltip(aggressive, locale::AI_AGGRESSIVE_DESC); + pos += vec2i(0, 30); + + @passive = GuiCheckbox(this, pos, locale::AI_PASSIVE); + setMarkupTooltip(passive, locale::AI_PASSIVE_DESC); + pos += vec2i(0, 30); + + @biased = GuiCheckbox(this, pos, locale::AI_BIASED); + setMarkupTooltip(biased, locale::AI_BIASED_DESC); + pos += vec2i(0, 30); + + @legacy = GuiCheckbox(this, pos, locale::AI_LEGACY); + legacy.textColor = Color(0xaaaaaaff); + legacy.visible = !hasDLC("Heralds"); + setMarkupTooltip(legacy, locale::AI_LEGACY_DESC); + pos += vec2i(0, 30); + + @cheatHeading = GuiText(this, Alignment(Left+260+165, Top+6, Right-12, Top+36)); + cheatHeading.font = FT_Medium; + cheatHeading.stroke = colors::Black; + cheatHeading.text = locale::AI_CHEATS; + + pos = recti_area(vec2i(260+165, 36), vec2i(170, 30)); + + @wealth = GuiCheckbox(this, recti_area(pos.topLeft, vec2i(110, 30)), locale::AI_WEALTH); + setMarkupTooltip(wealth, locale::AI_WEALTH_DESC); + @wealthAmt = GuiSpinbox(this, recti_area(pos.topLeft+vec2i(115, 0), vec2i(50, 30)), 10, 0, 1000, 1, 0); + pos += vec2i(0, 30); + + @strength = GuiCheckbox(this, recti_area(pos.topLeft, vec2i(110, 30)), locale::AI_STRENGTH); + setMarkupTooltip(strength, locale::AI_STRENGTH_DESC); + @strengthAmt = GuiSpinbox(this, recti_area(pos.topLeft+vec2i(115, 0), vec2i(50, 30)), 1, 0, 100, 1, 0); + pos += vec2i(0, 30); + + @abundance = GuiCheckbox(this, recti_area(pos.topLeft, vec2i(110, 30)), locale::AI_ABUNDANCE); + setMarkupTooltip(abundance, locale::AI_ABUNDANCE_DESC); + @abundanceAmt = GuiSpinbox(this, recti_area(pos.topLeft+vec2i(115, 0), vec2i(50, 30)), 1, 0, 100, 1, 0); + pos += vec2i(0, 30); + + @privileged = GuiCheckbox(this, pos, locale::AI_PRIVILEGED); + setMarkupTooltip(privileged, locale::AI_PRIVILEGED_DESC); + pos += vec2i(0, 30); + + @okButton = GuiButton(this, Alignment(Left+260+165, Bottom-34, Width=70, Height=30), locale::OK); + @applyToAllButton = GuiButton(this, Alignment(Left+260, Bottom-34, Width=120, Height=30), "Apply to all"); + + reset(); + } + + void reset() { + difficulties.selected = clamp(setup.settings.difficulty, 0, 2); + aggressive.checked = setup.settings.aiFlags & AIF_Aggressive != 0; + passive.checked = setup.settings.aiFlags & AIF_Passive != 0; + biased.checked = setup.settings.aiFlags & AIF_Biased != 0; + privileged.checked = setup.settings.aiFlags & AIF_CheatPrivileged != 0; + legacy.checked = setup.settings.type == ET_BumAI; + if(legacy.checked) + legacy.visible = true; + + wealth.checked = setup.settings.cheatWealth > 0; + wealthAmt.visible = wealth.checked; + if(wealth.checked) + wealthAmt.value = setup.settings.cheatWealth; + + strength.checked = setup.settings.cheatStrength > 0; + strengthAmt.visible = strength.checked; + if(strength.checked) + strengthAmt.value = setup.settings.cheatStrength; + + abundance.checked = setup.settings.cheatAbundance > 0; + abundanceAmt.visible = abundance.checked; + if(abundance.checked) + abundanceAmt.value = setup.settings.cheatAbundance; + } + + void apply(bool toAll = false) { + uint flags = 0; + if(aggressive.checked) + flags |= AIF_Aggressive; + if(passive.checked) + flags |= AIF_Passive; + if(biased.checked) + flags |= AIF_Biased; + if(privileged.checked) + flags |= AIF_CheatPrivileged; + + if(legacy.checked) + setup.settings.type = ET_BumAI; + else + setup.settings.type = ET_WeaselAI; + + wealthAmt.visible = wealth.checked; + if(wealthAmt.visible) + setup.settings.cheatWealth = wealthAmt.value; + else + setup.settings.cheatWealth = 0; + + strengthAmt.visible = strength.checked; + if(strengthAmt.visible) + setup.settings.cheatStrength = strengthAmt.value; + else + setup.settings.cheatStrength = 0; + + abundanceAmt.visible = abundance.checked; + if(abundanceAmt.visible) + setup.settings.cheatAbundance = abundanceAmt.value; + else + setup.settings.cheatAbundance = 0; + + if (toAll) { + for (uint i = 0; i < setup.ng.empires.length; i++) { + setup.ng.empires[i].settings.difficulty = difficulties.selected; + setup.ng.empires[i].settings.aiFlags = flags; + setup.ng.empires[i].submit(); + } + } else { + setup.settings.difficulty = difficulties.selected; + setup.settings.aiFlags = flags; + setup.submit(); + } + } + + bool onGuiEvent(const GuiEvent& evt) override { + if(evt.type == GUI_Changed) { + if(evt.caller is passive) { + if(passive.checked) + aggressive.checked = false; + apply(); + return true; + } + if(evt.caller is aggressive) { + if(aggressive.checked) + passive.checked = false; + apply(); + return true; + } + apply(); + } + if(evt.type == GUI_Clicked) { + if(evt.caller is okButton) { + apply(); + remove(); + return true; + } else if(evt.caller is applyToAllButton) { + apply(true); + remove(); + return true; + } + } + return BaseGuiElement::onGuiEvent(evt); + } + + void remove() { + overlay.remove(); + @overlay = null; + BaseGuiElement::remove(); + } + + void draw() override { + clearClip(); + skin.draw(SS_Panel, SF_Normal, AbsolutePosition); + BaseGuiElement::draw(); + } +}; + +class ChoosePopup : GuiIconGrid { + GuiOverlay@ overlay; + int extraHeight = 0; + + Color spriteColor; + array colors; + array sprites; + + ChoosePopup(const vec2i& pos, const vec2i& size, const vec2i& itemSize) { + @overlay = GuiOverlay(null); + overlay.closeSelf = false; + overlay.fade.a = 0; + super(overlay, recti_area(pos, size)); + horizAlign = 0.5; + vertAlign = 0.0; + iconSize = itemSize; + updateAbsolutePosition(); + } + + bool onGuiEvent(const GuiEvent& evt) override { + if(evt.caller is this && evt.type == GUI_Clicked) { + if(hovered != -1) + emitConfirmed(uint(hovered)); + overlay.close(); + return true; + } + return GuiIconGrid::onGuiEvent(evt); + } + + void remove() { + overlay.remove(); + @overlay = null; + GuiIconGrid::remove(); + } + + void add(const Color& col) { + colors.insertLast(col); + } + + void add(const Sprite& sprt) { + sprites.insertLast(sprt); + } + + uint get_length() override { + return max(colors.length, sprites.length); + } + + void drawElement(uint index, const recti& pos) override { + if(uint(hovered) == index) + drawRectangle(pos, Color(0xffffff30)); + if(index < colors.length) + drawRectangle(pos.padded(5), colors[index]); + if(index < sprites.length) + sprites[index].draw(pos, spriteColor); + } + + void draw() override { + clearClip(); + skin.draw(SS_Panel, SF_Normal, AbsolutePosition.padded(0,0,0,-extraHeight)); + GuiIconGrid::draw(); + } +}; + +class ColorPicker : BaseGuiElement { + Color picked; + bool pressed = false; + + ColorPicker(IGuiElement@ parent, const recti& pos) { + super(parent, pos); + updateAbsolutePosition(); + } + + void draw() { + shader::HSV_VALUE = 1.f; + shader::HSV_SAT_START = 0.5f; + shader::HSV_SAT_END = 1.f; + drawRectangle(AbsolutePosition, material::HSVPalette, Color()); + if(AbsolutePosition.isWithin(mousePos)) { + clearClip(); + recti area = recti_area(mousePos-vec2i(10), vec2i(20)); + drawRectangle(area.padded(-1), colors::Black); + drawRectangle(area, getColor(mousePos-AbsolutePosition.topLeft)); + } + BaseGuiElement::draw(); + } + + Color getColor(vec2i offset) { + Colorf col; + float hue = float(offset.x) / float(AbsolutePosition.width) * 360.f; + float sat = (1.f - float(offset.y) / float(AbsolutePosition.height)) * 0.5f + 0.5f; + col.fromHSV(hue, sat, 1.f); + col.a = 1.f; + return Color(col); + } + + bool onMouseEvent(const MouseEvent& event, IGuiElement@ source) { + if(event.type == MET_Button_Down || (event.type == MET_Moved && pressed)) { + pressed = true; + picked = getColor(mousePos - AbsolutePosition.topLeft); + + GuiEvent evt; + @evt.caller = this; + evt.type = GUI_Changed; + onGuiEvent(evt); + return true; + } + else if(pressed && event.type == MET_Button_Up) { + pressed = false; + emitConfirmed(); + return true; + } + return BaseGuiElement::onMouseEvent(event, source); + } +}; + +class PortraitChooser : GuiIconGrid { + array sprites; + uint selected = 0; + Color selectedColor; + + PortraitChooser(IGuiElement@ parent, Alignment@ align, const vec2i& itemSize) { + super(parent, align); + horizAlign = 0.5; + vertAlign = 0.0; + iconSize = itemSize; + updateAbsolutePosition(); + } + + void add(const Sprite& sprt) { + sprites.insertLast(sprt); + } + + uint get_length() override { + return sprites.length; + } + + void drawElement(uint index, const recti& pos) override { + if(selected == index) + drawRectangle(pos, selectedColor); + if(uint(hovered) == index) + drawRectangle(pos, Color(0xffffff30)); + if(index < sprites.length) + sprites[index].draw(pos); + } +}; + +class ShipsetChooser : GuiIconGrid { + array items; + uint selected = 0; + Color selectedColor; + + ShipsetChooser(IGuiElement@ parent, Alignment@ align, const vec2i& itemSize) { + super(parent, align); + horizAlign = 0.5; + vertAlign = 0.0; + iconSize = itemSize; + updateAbsolutePosition(); + } + + void add(const Shipset@ shipset) { + items.insertLast(shipset); + } + + uint get_length() override { + return items.length; + } + + void drawElement(uint index, const recti& pos) override { + if(selected == index) { + Color col = selectedColor; + col.a = 0x15; + drawRectangle(pos, col); + } + if(uint(hovered) == index) + drawRectangle(pos, Color(0xffffff15)); + if(index < items.length) { + const Shipset@ shipset = items[index]; + const Hull@ hull = shipset.hulls[0]; + if(hull !is null) { + quaterniond rot; + rot = quaterniond_fromAxisAngle(vec3d_front(), -0.9); + rot *= quaterniond_fromAxisAngle(vec3d_up(), 0.6); + rot *= quaterniond_fromAxisAngle(vec3d_right(), -0.5); + setClip(pos); + Color lightColor = colors::White; + if(selected == index) { + NODE_COLOR = Colorf(selectedColor); + lightColor = selectedColor; + } + else + NODE_COLOR = Colorf(1.f, 1.f, 1.f, 1.f); + drawLitModel(hull.model, hull.material, pos+vec2i(-4,0), rot, 1.9, lightColor=lightColor); + clearClip(); + } + + const Font@ ft = skin.getFont(FT_Bold); + if(selected == index || uint(hovered) == index) + ft.draw(text=shipset.name, pos=pos.padded(0,4), + horizAlign=0.5, vertAlign=0.0, stroke=colors::Black, + color=(selected == index ? selectedColor : colors::White)); + } + } +}; + +class WeaponSkinChooser : GuiIconGrid { + array items; + uint selected = 0; + Color selectedColor; + + WeaponSkinChooser(IGuiElement@ parent, Alignment@ align, const vec2i& itemSize) { + super(parent, align); + horizAlign = 0.5; + vertAlign = 0.0; + iconSize = itemSize; + updateAbsolutePosition(); + } + + void add(const EmpireWeaponSkin@ it) { + items.insertLast(it); + } + + uint get_length() override { + return items.length; + } + + void drawElement(uint index, const recti& pos) override { + if(selected == index) { + Color col = selectedColor; + col.a = 0x15; + drawRectangle(pos, col); + } + if(uint(hovered) == index) + drawRectangle(pos, Color(0xffffff15)); + if(index < items.length) + items[index].icon.draw(pos); + } +}; + +class TraitDisplay : BaseGuiElement { + const Trait@ trait; + GuiSprite@ icon; + GuiMarkupText@ name; + GuiMarkupText@ description; + GuiText@ points; + GuiText@ conflicts; + GuiCheckbox@ check; + bool hovered = false; + bool conflict = false; + + TraitDisplay(IGuiElement@ parent) { + super(parent, recti()); + + @icon = GuiSprite(this, Alignment(Left+20, Top+12, Left+52, Bottom-12)); + + @name = GuiMarkupText(this, Alignment(Left+65, Top+8, Right-168, Top+38)); + name.defaultFont = FT_Medium; + name.defaultStroke = colors::Black; + + @description = GuiMarkupText(this, Alignment(Left+124, Top+34, Right-168, Bottom-8)); + + @conflicts = GuiText(this, Alignment(Right-360, Top+8, Right-56, Bottom-8)); + conflicts.vertAlign = 0.1; + conflicts.horizAlign = 1.0; + + @points = GuiText(this, Alignment(Right-160, Top+8, Right-56, Bottom-8)); + points.horizAlign = 1.0; + points.font = FT_Subtitle; + + @check = GuiCheckbox(this, Alignment(Right-48, Top+0.5f-20, Right-8, Top+0.5f+20), ""); + } + + bool onGuiEvent(const GuiEvent& evt) override { + switch(evt.type) { + case GUI_Mouse_Entered: + hovered = true; + break; + case GUI_Mouse_Left: + hovered = false; + break; + case GUI_Changed: + if(evt.caller is check) { + check.checked = !check.checked; + emitClicked(); + return true; + } + break; + } + return BaseGuiElement::onGuiEvent(evt); + } + + bool onMouseEvent(const MouseEvent& evt, IGuiElement@ caller) override { + switch(evt.type) { + case MET_Button_Down: + if(evt.button == 0) + return true; + break; + case MET_Button_Up: + if(evt.button == 0) { + emitClicked(); + return true; + } + break; + } + return BaseGuiElement::onMouseEvent(evt, caller); + } + + void set(const Trait@ trait, bool selected, bool conflict) { + @this.trait = trait; + this.conflict = conflict; + description.text = trait.description; + icon.desc = trait.icon; + name.defaultColor = trait.color; + + if(trait.gives > 0) { + points.text = format(locale::RACE_POINTS_POS, toString(trait.gives)); + points.color = colors::Green; + points.visible = true; + } + else if(trait.cost > 0) { + points.text = format(locale::RACE_POINTS_NEG, toString(trait.cost)); + points.color = colors::Red; + points.visible = true; + } + else { + points.text = locale::RACE_POINTS_NEU; + points.color = Color(0xaaaaaaff); + points.visible = false; + } + + bool displayConflicts = false; + if(trait.conflicts.length > 0) { + if(conflict) { + conflicts.color = colors::Red; + conflicts.font = FT_Bold; + conflicts.vertAlign = 0.2; + } + else { + conflicts.color = Color(0xaaaaaaff); + conflicts.font = FT_Italic; + conflicts.vertAlign = 0.1; + } + string str = locale::CONFLICTS+" "; + for(uint i = 0, cnt = trait.conflicts.length; i < cnt; ++i) { + if(!trait.conflicts[i].available) + continue; + if(i != 0) + str += ", "; + str += trait.conflicts[i].name; + displayConflicts = true; + } + + conflicts.text = str; + } + if(displayConflicts) { + conflicts.visible = true; + points.vertAlign = 0.7; + } + else { + conflicts.visible = false; + points.vertAlign = 0.5; + } + + if(trait.unique.length != 0) { + check.style = SS_Radiobox; + if(description.alignment.right.pixels != 52) { + description.alignment.right.pixels = 52; + description.updateAbsolutePosition(); + } + } + else { + check.style = SS_Checkbox; + if(description.alignment.right.pixels != 168) { + description.alignment.right.pixels = 168; + description.updateAbsolutePosition(); + } + } + + name.text = trait.name; + check.checked = selected; + } + + void draw() { + if(check.checked) + skin.draw(SS_Glow, SF_Normal, AbsolutePosition, trait.color); + skin.draw(SS_Panel, SF_Normal, AbsolutePosition.padded(4), trait.color); + if(hovered) + drawRectangle(AbsolutePosition.padded(8), Color(0xffffff10)); + BaseGuiElement::draw(); + } +}; + +class SaveRaceDialog : SaveDialog { + EmpireSettings settings; + EmpireSetup@ setup; + + SaveRaceDialog(IGuiElement@ bind, EmpireSettings@ settings, EmpireSetup@ setup) { + this.settings = settings; + @this.setup = setup; + super(bind, modProfile["races"], settings.raceName+".race"); + } + + void clickConfirm() override { + exportRace(settings, path); + } +}; + +class LoadRaceDialog : LoadDialog { + EmpireSettings settings; + EmpireSetup@ setup; + TraitsWindow@ win; + + LoadRaceDialog(TraitsWindow@ win, EmpireSettings@ settings, EmpireSetup@ setup) { + this.settings = settings; + @this.setup = setup; + @this.win = win; + super(win, modProfile["races"]); + } + + void clickConfirm() override { + importRace(setup.settings, path); + if(win !is null) + win.update(); + setup.submit(); + } +}; + +class TraitElement : GuiListElement { + const Trait@ trait; + + void draw(GuiListbox@ ele, uint flags, const recti& absPos) { + recti iconPos = recti_area(absPos.topLeft+vec2i(10, 5), vec2i(absPos.height-10, absPos.height-10)); + trait.icon.draw(iconPos); + + recti textPos = absPos.padded(absPos.height + 10, 0, 10, 4); + ele.skin.getFont(FT_Medium).draw( + text=trait.name, pos=textPos); + } + + string get_tooltipText() { + return format("[color=$1][b]$2[/b][/color]\n$3", + toString(trait.color), trait.name, trait.description); + } +}; + +class TraitsWindow : BaseGuiElement { + GuiOverlay@ overlay; + EmpireSetup@ setup; + + GuiBackgroundPanel@ bg; + + GuiListbox@ categories; + array usedCategories; + + GuiPanel@ profilePanel; + + GuiText@ nameLabel; + GuiTextbox@ name; + + GuiText@ portraitLabel; + PortraitChooser@ portrait; + + GuiText@ shipsetLabel; + ShipsetChooser@ shipset; + + GuiText@ weaponSkinLabel; + WeaponSkinChooser@ weaponSkin; + + GuiText@ traitsLabel; + GuiListbox@ traitList; + + GuiText@ pointsLabel; + + GuiPanel@ traitPanel; + GuiText@ noTraits; + array traits; + + GuiButton@ acceptButton; + GuiButton@ saveButton; + GuiButton@ loadButton; + + TraitsWindow(EmpireSetup@ setup) { + @this.setup = setup; + @overlay = GuiOverlay(null); + overlay.closeSelf = false; + super(overlay, Alignment(Left+0.11f, Top+0.11f, Right-0.11f, Bottom-0.11f)); + updateAbsolutePosition(); + + @bg = GuiBackgroundPanel(this, Alignment().fill()); + bg.titleColor = Color(0xff8000ff); + bg.title = locale::CUSTOMIZE_RACE; + + @categories = GuiListbox(bg, Alignment(Left+4, Top+32, Left+250, Bottom-4)); + categories.itemHeight = 44; + categories.style = SS_PlainOverlay; + categories.itemStyle = SS_TabButton; + categories.addItem(GuiMarkupListText(locale::RACE_PROFILE)); + categories.required = true; + + for(uint i = 0, cnt = getTraitCategoryCount(); i < cnt; ++i) { + auto@ cat = getTraitCategory(i); + bool hasTraits = false; + for(uint n = 0, ncnt = getTraitCount(); n < ncnt; ++n) { + if(getTrait(n).category is cat && getTrait(n).available && getTrait(n).hasDLC) { + hasTraits = true; + break; + } + } + if(hasTraits) { + categories.addItem(GuiMarkupListText(cat.name)); + usedCategories.insertLast(cat); + } + } + + @acceptButton = GuiButton(bg, Alignment(Right-140, Bottom-40, Right-3, Bottom-3), locale::ACCEPT); + @loadButton = GuiButton(bg, Alignment(Right-274, Bottom-40, Right-154, Bottom-3), locale::LOAD); + @saveButton = GuiButton(bg, Alignment(Right-400, Bottom-40, Right-280, Bottom-3), locale::SAVE); + @pointsLabel = GuiText(bg, Alignment(Left+264, Bottom-40, Right-410, Bottom-3)); + pointsLabel.font = FT_Medium; + + Alignment panelAlign(Left+258, Top+32, Right-4, Bottom-40); + + @profilePanel = GuiPanel(bg, panelAlign); + @traitPanel = GuiPanel(bg, panelAlign); + traitPanel.visible = false; + + int y = 8; + + @nameLabel = GuiText(profilePanel, Alignment(Left+12, Top+y, Left+200, Top+y+30), locale::RACE_NAME, FT_Bold); + @name = GuiTextbox(profilePanel, Alignment(Left+200, Top+y, Right-12, Top+y+30), setup.settings.raceName); + y += 38; + + int h = 80 + (getEmpirePortraitCount() / ((size.width - 200) / 70)) * 80; + @portraitLabel = GuiText(profilePanel, Alignment(Left+12, Top+y, Left+200, Top+y+30), locale::PORTRAIT, FT_Bold); + @portrait = PortraitChooser(profilePanel, Alignment(Left+200, Top+y, Right-12, Top+y+h), vec2i(70, 70)); + portrait.selectedColor = setup.settings.color; + + portrait.selected = randomi(0, getEmpirePortraitCount()-1); + portrait.horizAlign = 0.0; + for(uint i = 0, cnt = getEmpirePortraitCount(); i < cnt; ++i) { + auto@ img = getEmpirePortrait(i); + portrait.add(Sprite(img.portrait)); + if(img.ident == setup.settings.portrait) + portrait.selected = i; + } + y += h+8; + + h = 80 + (getShipsetCount() / ((size.width - 200) / 150)) * 80; + @shipsetLabel = GuiText(profilePanel, Alignment(Left+12, Top+y, Left+200, Top+y+30), locale::SHIPSET, FT_Bold); + @shipset = ShipsetChooser(profilePanel, Alignment(Left+200, Top+y, Right-12, Top+y+h), vec2i(150, 70)); + shipset.selectedColor = setup.settings.color; + shipset.selected = 0; + shipset.horizAlign = 0.0; + for(uint i = 0, cnt = getShipsetCount(); i < cnt; ++i) { + auto@ ss = getShipset(i); + if(ss.available && (ss.dlc.length == 0 || hasDLC(ss.dlc))) + shipset.add(ss); + if(ss.ident == setup.settings.shipset) + shipset.selected = shipset.length-1; + } + y += h+8; + + @weaponSkinLabel = GuiText(profilePanel, Alignment(Left+12, Top+y, Left+200, Top+y+30), locale::WEAPON_SKIN, FT_Bold); + @weaponSkin = WeaponSkinChooser(profilePanel, Alignment(Left+200, Top+y, Right-12, Top+y+80), vec2i(120, 70)); + weaponSkin.selectedColor = setup.settings.color; + weaponSkin.selected = 0; + weaponSkin.horizAlign = 0.0; + for(uint i = 0, cnt = getEmpireWeaponSkinCount(); i < cnt; ++i) { + auto@ skin = getEmpireWeaponSkin(i); + weaponSkin.add(skin); + if(skin.ident == setup.settings.effectorSkin) + weaponSkin.selected = weaponSkin.length-1; + } + y += 88; + + @traitsLabel = GuiText(profilePanel, Alignment(Left+12, Top+y, Left+200, Top+y+30), locale::TRAITS, FT_Bold); + @traitList = GuiListbox(profilePanel, Alignment(Left+200, Top+y, Right-12, Bottom-8)); + traitList.itemStyle = SS_StaticListboxItem; + traitList.itemHeight = 50; + addLazyMarkupTooltip(traitList); + @noTraits = GuiText(profilePanel, Alignment(Left+240, Top+y+10, Right-12, Top+y+50), locale::NO_TRAITS); + noTraits.color = Color(0xaaaaaaff); + noTraits.vertAlign = 0.0; + y += 58; + + update(); + updateAbsolutePosition(); + } + + void update() { + int sel = categories.selected; + profilePanel.visible = sel == 0; + traitPanel.visible = sel != 0; + + uint index = 0; + const TraitCategory@ cat; + if(sel > 0) + @cat = usedCategories[sel - 1]; + + int points = STARTING_TRAIT_POINTS; + for(uint i = 0, cnt = setup.settings.traits.length; i < cnt; ++i) { + points += setup.settings.traits[i].gives; + points -= setup.settings.traits[i].cost; + } + + if(traitPanel.visible) { + int y = 0; + array list; + for(uint i = 0, cnt = getTraitCount(); i < cnt; ++i) { + auto@ trait = getTrait(i); + if(cat !is null && cat !is trait.category) + continue; + if(!setup.player && !trait.aiSupport) + continue; + if(!trait.available) + continue; + if(!trait.hasDLC) + continue; + list.insertLast(trait); + } + list.sortAsc(); + + for(uint i = 0, cnt = list.length; i < cnt; ++i) { + auto@ trait = list[i]; + TraitDisplay@ disp; + if(index < traits.length) { + @disp = traits[index]; + } + else { + @disp = TraitDisplay(traitPanel); + traits.insertLast(disp); + } + + disp.set(trait, setup.settings.hasTrait(trait), trait.hasConflicts(setup.settings.traits)); + disp.alignment.set(Left, Top+y, Right, Top+y+140); + disp.updateAbsolutePosition(); + int needH = disp.description.renderer.height+48; + if(needH != 140) { + disp.alignment.set(Left, Top+y, Right, Top+y+needH); + disp.updateAbsolutePosition(); + } + + ++index; + y += needH; + } + + for(uint i = index, cnt = traits.length; i < cnt; ++i) + traits[i].remove(); + traits.length = index; + traitPanel.updateAbsolutePosition(); + } + + if(profilePanel.visible) { + uint cnt = setup.settings.traits.length; + traitList.removeItemsFrom(cnt); + for(uint i = 0; i < cnt; ++i) { + auto@ item = cast(traitList.getItemElement(i)); + if(item is null) { + @item = TraitElement(); + traitList.addItem(item); + } + + @item.trait = setup.settings.traits[i]; + } + noTraits.visible = cnt == 0; + } + + if(points > 0) { + pointsLabel.color = colors::Green; + pointsLabel.text = format(locale::RACE_POINTS_AVAIL_POS, toString(points)); + pointsLabel.visible = true; + } + else if(points < 0) { + pointsLabel.color = colors::Red; + pointsLabel.text = format(locale::RACE_POINTS_AVAIL_NEG, toString(-points)); + pointsLabel.visible = true; + } + else { + pointsLabel.color = Color(0xaaaaaaff); + pointsLabel.text = format(locale::RACE_POINTS_AVAIL_POS, toString(points)); + pointsLabel.visible = false; + } + + if(points >= 0 && !setup.settings.hasTraitConflicts()) + acceptButton.color = colors::Green; + else + acceptButton.color = colors::Red; + } + + bool onGuiEvent(const GuiEvent& evt) override { + if(evt.caller is acceptButton) { + if(evt.type == GUI_Clicked) { + overlay.close(); + return true; + } + } + if(evt.caller is saveButton) { + if(evt.type == GUI_Clicked) { + SaveRaceDialog(this, setup.settings, setup); + return true; + } + } + else if(evt.caller is loadButton) { + if(evt.type == GUI_Clicked) { + LoadRaceDialog(this, setup.settings, setup); + return true; + } + } + if(evt.type == GUI_Clicked) { + if(evt.caller is portrait) { + int hov = portrait.hovered; + if(hov >= 0) { + setup.settings.portrait = getEmpirePortrait(hov).ident; + portrait.selected = hov; + } + setup.submit(); + return true; + } + if(evt.caller is shipset) { + int hov = shipset.hovered; + if(hov >= 0) { + setup.settings.shipset = shipset.items[hov].ident; + shipset.selected = hov; + } + setup.submit(); + return true; + } + if(evt.caller is weaponSkin) { + int hov = weaponSkin.hovered; + if(hov >= 0) { + setup.settings.effectorSkin = weaponSkin.items[hov].ident; + weaponSkin.selected = hov; + } + setup.submit(); + return true; + } + + auto@ disp = cast(evt.caller); + if(disp !is null) { + if(disp.trait.unique.length != 0) + setup.settings.chooseTrait(disp.trait); + else if(setup.settings.hasTrait(disp.trait)) + setup.settings.removeTrait(disp.trait); + else + setup.settings.addTrait(disp.trait); + update(); + setup.submit(); + } + } + if(evt.type == GUI_Changed) { + if(evt.caller is name) { + setup.settings.raceName = name.text; + setup.submit(); + return true; + } + if(evt.caller is categories) { + update(); + return true; + } + } + return BaseGuiElement::onGuiEvent(evt); + } + + void draw() override { + BaseGuiElement::draw(); + } +}; + +class GalaxySetup : BaseGuiElement { + Map@ mp; + NewGame@ ng; + GuiText@ name; + + GuiText@ timesLabel; + GuiSpinbox@ timesBox; + + GuiPanel@ settings; + + GuiButton@ removeButton; + GuiButton@ hwButton; + GuiSprite@ hwX; + + GalaxySetup(NewGame@ menu, Alignment@ align, Map@ fromMap) { + super(menu.galaxyPanel, align); + @mp = fromMap.create(); + @ng = menu; + + @name = GuiText(this, Alignment(Left+6, Top+5, Right-262, Height=28)); + name.text = mp.name; + name.font = FT_Medium; + name.color = mp.color; + name.stroke = colors::Black; + + @timesBox = GuiSpinbox(this, Alignment(Right-190, Top+7, Width=52, Height=22), 1.0); + timesBox.min = 1.0; + timesBox.max = 100.0; + timesBox.decimals = 0; + timesBox.color = Color(0xffffff60); + + @timesLabel = GuiText(this, Alignment(Right-135, Top+7, Width=25, Height=22), "x"); + + timesBox.visible = !mp.isUnique; + timesLabel.visible = !mp.isUnique; + + @removeButton = GuiButton(this, Alignment(Right-84, Top+4, Right-25, Top+34)); + removeButton.setIcon(icons::Remove); + removeButton.color = colors::Red; + + @hwButton = GuiButton(this, Alignment(Right-230, Top+5, Width=26, Height=26)); + hwButton.setIcon(Sprite(spritesheet::PlanetType, 2, Color(0xffffffaa)), padding=0); + hwButton.toggleButton = true; + hwButton.pressed = false; + hwButton.style = SS_IconButton; + hwButton.color = Color(0xff0000ff); + setMarkupTooltip(hwButton, locale::NGTT_MAP_HW); + @hwX = GuiSprite(hwButton, Alignment(), Sprite(spritesheet::QuickbarIcons, 3, Color(0xffffff80))); + hwX.visible = false; + + @settings = GuiPanel(this, + Alignment(Left, Top+42, Right, Bottom-4)); + mp.create(settings); + } + + void setHomeworlds(bool value) { + hwButton.pressed = !value; + hwX.visible = hwButton.pressed; + if(hwButton.pressed) + hwButton.fullIcon.color = Color(0xffffffff); + else + hwButton.fullIcon.color = Color(0xffffffaa); + } + + void apply(MapSettings& set) { + set.map_id = mp.id; + set.galaxyCount = timesBox.value; + @set.parent = ng.settings; + set.allowHomeworlds = !hwButton.pressed; + mp.apply(set); + } + + void load(MapSettings& set) { + auto@ _map = getMap(set.map_id); + if(getClass(mp) !is getClass(_map)) + @mp = cast(getClass(_map).create()); + timesBox.value = set.galaxyCount; + + hwButton.pressed = !set.allowHomeworlds; + hwX.visible = hwButton.pressed; + if(hwButton.pressed) + hwButton.fullIcon.color = Color(0xffffffff); + else + hwButton.fullIcon.color = Color(0xffffffaa); + + mp.load(set); + } + + bool onGuiEvent(const GuiEvent& evt) { + switch(evt.type) { + case GUI_Clicked: + if(evt.caller is removeButton) { + ng.removeGalaxy(this); + return true; + } + else if(evt.caller is hwButton) { + setHomeworlds(!hwButton.pressed); + return true; + } + break; + } + return BaseGuiElement::onGuiEvent(evt); + } + + void draw() { + recti bgPos = AbsolutePosition.padded(-5,0,-4,0); + clipParent(bgPos); + skin.draw(SS_GalaxySetupItem, SF_Normal, bgPos.padded(-4,0), mp.color); + resetClip(); + auto@ icon = mapIcons[mp.index]; + if(mp.icon.length != 0 && icon.isLoaded(0)) { + recti pos = AbsolutePosition.padded(0,42,0,0).aspectAligned(1.0, horizAlign=1.0, vertAlign=1.0); + icon.draw(pos, Color(0xffffff80)); + } + BaseGuiElement::draw(); + } +}; + +class MapElement : GuiListElement { + Map@ mp; + + MapElement(Map@ _map) { + @mp = _map; + } + + void draw(GuiListbox@ ele, uint flags, const recti& absPos) override { + const Font@ title = ele.skin.getFont(FT_Subtitle); + const Font@ normal = ele.skin.getFont(FT_Normal); + + ele.skin.draw(SS_ListboxItem, flags, absPos, mp.color); + auto@ icon = mapIcons[mp.index]; + if(mp.icon.length != 0 && icon.isLoaded(0)) { + recti pos = absPos.aspectAligned(1.0, horizAlign=1.0, vertAlign=1.0); + icon.draw(pos, Color(0xffffff80)); + } + + title.draw(pos=absPos.resized(0, 32).padded(12,4), + text=mp.name, color=mp.color, stroke=colors::Black); + normal.draw(pos=absPos.padded(12,36,12+absPos.height,0), offset=vec2i(), + lineHeight=-1, text=mp.description, color=colors::White); + } +}; + +class Quickstart : ConsoleCommand { + void execute(const string& args) { + new_game.start(); + } +}; + +NewGame@ new_game; +array mapIcons; + +void init() { + @new_game = NewGame(); + new_game.visible = false; + + addConsoleCommand("quickstart", Quickstart()); +} + +array connectedPlayers; +set_int connectedSet; +void tick(double time) { + if(new_game.visible) + new_game.tick(time); + if(!game_running && mpServer) { + array@ players = getPlayers(); + + //Send connect events + for(uint i = 0, cnt = players.length; i < cnt; ++i) { + Player@ pl = players[i]; + if(pl.id == CURRENT_PLAYER.id) + continue; + string name = pl.name; + if(name.length == 0) + continue; + if(!connectedSet.contains(pl.id)) { + string msg = format("[color=#aaa]* "+locale::MP_CONNECT_EVENT+"[/color]", + format("[b]$1[/b]", bbescape(name))); + recvMenuJoin(ALL_PLAYERS, msg); + connectedPlayers.insertLast(pl); + connectedSet.insert(pl.id); + } + } + + connectedSet.clear(); + for(uint i = 0, cnt = players.length; i < cnt; ++i) + connectedSet.insert(players[i].id); + + //Send disconnect events + for(uint i = 0, cnt = connectedPlayers.length; i < cnt; ++i) { + if(!connectedSet.contains(connectedPlayers[i].id)) { + Color color; + string name = connectedPlayers[i].name; + + string msg = format("[color=#aaa]* "+locale::MP_DISCONNECT_EVENT+"[/color]", + format("[b]$2[/b]", toString(color), bbescape(name))); + recvMenuLeave(ALL_PLAYERS, msg); + connectedPlayers.removeAt(i); + --i; --cnt; + } + } + } +} + +void showNewGame(bool fromMP = false) { + new_game.visible = true; + new_game.fromMP = fromMP; + new_game.init(); + menu_container.visible = false; + menu_container.animateOut(); + new_game.animateIn(); +} + +void hideNewGame(bool snap = false) { + new_game.fromMP = false; + menu_container.visible = true; + if(!snap) { + menu_container.animateIn(); + new_game.animateOut(); + } + else { + animate_remove(new_game); + new_game.visible = false; + menu_container.show(); + } +} + +void changeEmpireSettings_client(Player& pl, EmpireSettings@ settings) { + auto@ emp = new_game.findPlayer(pl.id); + emp.settings.raceName = settings.raceName; + emp.settings.traits = settings.traits; + emp.settings.portrait = settings.portrait; + emp.settings.shipset = settings.shipset; + emp.settings.effectorSkin = settings.effectorSkin; + emp.settings.color = settings.color; + emp.settings.flag = settings.flag; + emp.settings.ready = settings.ready; + emp.settings.team = settings.team; + emp.update(); +} + +bool sendPeriodic(Message& msg) { + if(game_running) + return false; + new_game.apply(); + msg << new_game.settings; + return true; +} + +void recvPeriodic(Message& msg) { + msg >> new_game.settings; + new_game.reset(); + new_game.updateAbsolutePosition(); +} + +void chatMessage(Player& pl, string text) { + auto@ emp = new_game.findPlayer(pl.id); + Color color = emp.settings.color; + string msg = format("[b][color=$1]$2[/color][/b] [offset=100]$3[/offset]", + toString(color), bbescape(emp.name.text), bbescape(text)); + recvMenuChat(ALL_PLAYERS, msg); +} + +void chatMessage_client(string text) { + new_game.addChat(text); + sound::generic_click.play(); +} + +void chatJoin_client(string text) { + new_game.addChat(text); + sound::generic_ok.play(); +} + +void chatLeave_client(string text) { + new_game.addChat(text); + sound::generic_warn.play(); +} ADDED scripts/server/cheats.as Index: scripts/server/cheats.as ================================================================== --- scripts/server/cheats.as +++ scripts/server/cheats.as @@ -0,0 +1,231 @@ +import orbitals; +import object_creation; +import tile_resources; +import void setInstantColonize(bool) from "planets.SurfaceComponent"; +from empire import sendChatMessage; + +import influence; +from bonus_effects import BonusEffect; +from generic_effects import GenericEffect; +import hooks; + +bool CHEATS_ENABLED_THIS_GAME = false; +bool CHEATS_ENABLED = false; +bool getCheatsEnabled() { + return CHEATS_ENABLED; +} + +bool getCheatsEverOn() { + return CHEATS_ENABLED_THIS_GAME; +} + +void setCheatsEnabled(Player& player, bool enabled) { + if(player != HOST_PLAYER) + return; + CHEATS_ENABLED = enabled; + if(enabled) + CHEATS_ENABLED_THIS_GAME = true; + cheatsEnabled(ALL_PLAYERS, enabled); + if(mpServer) { + if(enabled) + sendChatMessage(locale::MP_CHEATS_ENABLED, color=Color(0xaaaaaaff), offset=30); + else + sendChatMessage(locale::MP_CHEATS_DISABLED, color=Color(0xaaaaaaff), offset=30); + } +} + +void cheatSeeAll(Player& player, bool enabled) { + if(!CHEATS_ENABLED || player.emp is null || !player.emp.valid) + return; + player.emp.visionMask = enabled ? ~0 : player.emp.mask; +} + +void cheatColonize(bool enabled) { + if(!CHEATS_ENABLED) + return; + setInstantColonize(enabled); +} + +void cheatSpawnFlagship(Object@ spawnAt, const Design@ design, Empire@ owner) { + if(!CHEATS_ENABLED) + return; + if(design.hasTag(ST_IsSupport)) + return; + createShip(spawnAt, design, owner, free=true); +} + +void cheatSpawnFlagship(vec3d spawnAt, const Design@ design, Empire@ owner) { + if(!CHEATS_ENABLED) + return; + if(design.hasTag(ST_IsSupport)) + return; + Ship@ ship = createShip(spawnAt, design, owner, free=true); + ship.addMoveOrder(spawnAt); +} + +void cheatSpawnSupports(Object@ spawnAt, const Design@ design, uint count) { + if(!CHEATS_ENABLED) + return; + if(!design.hasTag(ST_IsSupport)) + return; + if(!spawnAt.hasLeaderAI || spawnAt.owner is null || !spawnAt.owner.valid) + return; + for(uint i = 0; i < count; ++i) + createShip(spawnAt, design, spawnAt.owner); +} + +void cheatSpawnSupports(vec3d spawnAt, const Design@ design, uint count, Empire@ owner) { + if(!CHEATS_ENABLED) + return; + if(!design.hasTag(ST_IsSupport)) + return; + for(uint i = 0; i < count; ++i) + createShip(spawnAt, design, owner); +} + +void cheatSpawnOrbital(vec3d spawnAt, uint orbitalType, Empire@ owner) { + if(!CHEATS_ENABLED) + return; + const OrbitalModule@ def = getOrbitalModule(orbitalType); + if(def is null) + return; + createOrbital(spawnAt, def, owner); +} + +void cheatInfluence(Player& player, int amount) { + if(!CHEATS_ENABLED || player.emp is null || !player.emp.valid) + return; + player.emp.addInfluence(amount); +} + +void cheatResearch(Player& player, double amount) { + if(!CHEATS_ENABLED || player.emp is null || !player.emp.valid) + return; + player.emp.generatePoints(amount); +} + +void cheatMoney(Player& player, int amount) { + if(!CHEATS_ENABLED || player.emp is null || !player.emp.valid) + return; + player.emp.addBonusBudget(amount); +} + +void cheatEnergy(Player& player, int amount) { + if(!CHEATS_ENABLED || player.emp is null || !player.emp.valid) + return; + player.emp.modEnergyStored(amount); +} + +void cheatFTL(Player& player, int amount) { + if(!CHEATS_ENABLED || player.emp is null || !player.emp.valid) + return; + if(player.emp.FTLCapacity < amount) + player.emp.modFTLCapacity(amount); + player.emp.modFTLStored(amount); +} + +void cheatActivateAI(Player& player) { + if(!CHEATS_ENABLED || player.emp is null || !player.emp.valid) + return; + player.emp.initBasicAI(); +} + +void cheatDebugAI(Empire@ emp) { + if(!CHEATS_ENABLED || emp is null) + return; + emp.debugAI(); +} + +void commandPlayerAI(string cmd) { + playerEmpire.commandAI(cmd); +} + +void cheatCommandAI(Empire@ emp, string cmd) { + if(emp is playerEmpire) { + if (cmd == "no achievements") { + CHEATS_ENABLED_THIS_GAME = true; + } + else { + emp.commandAI(cmd); + } + return; + } + if(!CHEATS_ENABLED || emp is null) + return; + emp.commandAI(cmd); +} + +void cheatTrigger(Player& player, Object@ obj, Empire@ emp, string hook) { + Empire@ plEmp = player.emp; + if(!CHEATS_ENABLED || plEmp is null || !plEmp.valid) + return; + BonusEffect@ trig = cast(parseHook(hook, "bonus_effects::", required=false)); + if(trig !is null) { + trig.activate(obj, emp); + return; + } + GenericEffect@ eff = cast(parseHook(hook, "planet_effects::")); + if(eff !is null) { + eff.enable(obj, null); + return; + } +} + +void cheatChangeOwner(Object@ obj, Empire@ newOwner) { + if(!CHEATS_ENABLED || obj is null || newOwner is null) + return; + if(obj.isPlanet) { + obj.takeoverPlanet(newOwner); + } + else if(obj.isShip) { + if(obj.hasLeaderAI) { + uint cnt = obj.supportCount; + for(uint i = 0; i < cnt; ++i) + @obj.supportShip[i].owner = newOwner; + } + + @obj.owner = newOwner; + } + else { + @obj.owner = newOwner; + } +} + +void cheatAlliance(Empire& from, Empire& to) { + if(!CHEATS_ENABLED) + return; + if(from is to) + return; + if(!from.valid || !to.valid) + return; +} + +void cheatDestroy(Object@ obj) { + if(!CHEATS_ENABLED || obj is null) + return; + obj.destroy(); +} + +void cheatLabor(Object@ obj, double amount) { + if(!CHEATS_ENABLED || obj is null) + return; + obj.modLaborIncome(amount); +} + +void syncInitial(Message& msg) { + msg << CHEATS_ENABLED; + msg << CHEATS_ENABLED_THIS_GAME; +} + +void save(SaveFile& msg) { + msg << CHEATS_ENABLED; + msg << CHEATS_ENABLED_THIS_GAME; +} + +void load(SaveFile& msg) { + msg >> CHEATS_ENABLED; + if(msg >= SV_0025) + msg >> CHEATS_ENABLED_THIS_GAME; + else + CHEATS_ENABLED_THIS_GAME = CHEATS_ENABLED; +} ADDED scripts/server/empire_ai/EmpireAI.as Index: scripts/server/empire_ai/EmpireAI.as ================================================================== --- scripts/server/empire_ai/EmpireAI.as +++ scripts/server/empire_ai/EmpireAI.as @@ -0,0 +1,170 @@ +import settings.game_settings; + +import AIController@ createBumAI() from "empire_ai.BumAI"; +import AIController@ createBasicAI() from "empire_ai.BasicAI"; +import AIController@ createWeaselAI() from "empire_ai.weasel.WeaselAI"; + +interface AIController { + void debugAI(); + void commandAI(string cmd); + void aiPing(Empire@ fromEmpire, vec3d position, uint type); + void init(Empire& emp, EmpireSettings& settings); + void init(Empire& emp); + void tick(Empire& emp, double time); + void pause(Empire& emp); + void resume(Empire& emp); + void load(SaveFile& msg); + void save(SaveFile& msg); + int getDifficultyLevel(); + vec3d get_aiFocus(); + string getOpinionOf(Empire& emp, Empire@ other); + int getStandingTo(Empire& emp, Empire@ other); +} + +class EmpireAI : Component_EmpireAI, Savable { + AIController@ ctrl; + uint aiType; + bool paused = false; + bool override = true; + + EmpireAI() { + } + + vec3d get_aiFocus() { + if(ctrl !is null) + return ctrl.aiFocus; + else + return vec3d(); + } + + int get_difficulty() { + if(ctrl !is null) + return ctrl.getDifficultyLevel(); + else + return -1; + } + + bool get_isAI(Empire& emp) { + return ctrl !is null && (emp.player is null || override); + } + + string getRelation(Player& pl, Empire& emp) { + auto@ other = pl.emp; + if(ctrl is null || other is null || !other.valid || other.ContactMask.value & emp.mask == 0) + return ""; + else + return ctrl.getOpinionOf(emp, other); + } + + int getRelationState(Player& pl, Empire& emp) { + auto@ other = pl.emp; + if(ctrl is null || other is null || !other.valid || other.ContactMask.value & emp.mask == 0) + return 0; + else + return ctrl.getStandingTo(emp, other); + } + + uint getAIType() { + return aiType; + } + + void debugAI() { + if(ctrl !is null) + ctrl.debugAI(); + } + + void commandAI(string cmd) { + if(ctrl !is null) + ctrl.commandAI(cmd); + } + + void load(SaveFile& msg) { + msg >> paused; + msg >> override; + msg >> aiType; + + createAI(aiType); + if(ctrl !is null) + ctrl.load(msg); + } + + void save(SaveFile& msg) { + msg << paused; + msg << override; + msg << aiType; + + if(ctrl !is null) + ctrl.save(msg); + } + + void createAI(uint type) { + aiType = type; + + //Create the controller + switch(type) { + case ET_Player: + //Do nothing + break; + case ET_BumAI: + @ctrl = createBasicAI(); + break; + case ET_WeaselAI: + @ctrl = createWeaselAI(); + break; + } + + } + + void aiPing(Empire@ fromEmpire, vec3d position, uint type = 0) { + if(ctrl !is null) + ctrl.aiPing(fromEmpire, position, type); + } + + void init(Empire& emp, EmpireSettings& settings) { + createAI(settings.type); + + //Initialize + if(ctrl !is null) + ctrl.init(emp, settings); + } + + void initBasicAI(Empire& emp) { + override = true; + if(ctrl !is null) + return; + + createAI(ET_WeaselAI); + + if(ctrl !is null) { + EmpireSettings settings; + settings.difficulty = 2; + settings.aiFlags |= AIF_Aggressive; + ctrl.init(emp, settings); + ctrl.init(emp); + } + } + + void init(Empire& emp) { + if(ctrl !is null) + ctrl.init(emp); + } + + void aiTick(Empire& emp, double tick) { + if(ctrl is null) + return; + + if(emp.player is null || override) { + if(paused) { + ctrl.resume(emp); + paused = false; + } + ctrl.tick(emp, tick); + } + else { + if(!paused) { + ctrl.pause(emp); + paused = true; + } + } + } +}; ADDED scripts/server/empire_ai/weasel/Budget.as Index: scripts/server/empire_ai/weasel/Budget.as ================================================================== --- scripts/server/empire_ai/weasel/Budget.as +++ scripts/server/empire_ai/weasel/Budget.as @@ -0,0 +1,472 @@ +// Budget +// ------ +// Tasked with managing the empire's money and making sure we have enough to spend +// on various things, as well as dealing with prioritization and budget allocation. +// + +import empire_ai.weasel.WeaselAI; + +enum BudgetType { + BT_Military, + BT_Infrastructure, + BT_Colonization, + BT_Development, + + BT_COUNT +}; + +final class AllocateBudget { + int id = -1; + uint type; + int cost = 0; + int maintenance = 0; + + double requestTime = 0; + double priority = 1; + + bool allocated = false; + int opCmp(const AllocateBudget@ other) const { + if(priority < other.priority) + return -1; + if(priority > other.priority) + return 1; + if(requestTime < other.requestTime) + return 1; + if(requestTime > other.requestTime) + return -1; + return 0; + } + + void save(SaveFile& file) { + file << id; + file << type; + file << cost; + file << maintenance; + file << requestTime; + file << priority; + file << allocated; + } + + void load(SaveFile& file) { + file >> id; + file >> type; + file >> cost; + file >> maintenance; + file >> requestTime; + file >> priority; + file >> allocated; + } +}; + +final class BudgetPart { + uint type; + + array allocations; + + //How much we've spent this cycle + int spent = 0; + + //How much is remaining to be spent this cycle + int remaining = 0; + + //How much maintenance we've gained this cycle + int gainedMaintenance = 0; + + //How much maintenance we can still gain this cycle + int remainingMaintenance = 0; + + void update(AI& ai, Budget& budget) { + + for(uint i = 0, cnt = allocations.length; i < cnt; ++i) { + auto@ alloc = allocations[i]; + if(alloc.priority < 1.0) { + if(alloc.cost >= remaining && alloc.maintenance >= remainingMaintenance) { + budget.spend(type, alloc.cost, alloc.maintenance); + alloc.allocated = true; + allocations.removeAt(i); + break; + } + } + else { + if(budget.canSpend(type, alloc.cost, alloc.maintenance, alloc.priority)) { + budget.spend(type, alloc.cost, alloc.maintenance); + alloc.allocated = true; + allocations.removeAt(i); + break; + } + } + } + } + + void turn(AI& ai, Budget& budget) { + spent = 0; + remaining = 0; + + gainedMaintenance = 0; + remainingMaintenance = 0; + } + + void save(Budget& budget, SaveFile& file) { + file << spent; + file << remaining; + file << gainedMaintenance; + file << remainingMaintenance; + + uint cnt = allocations.length; + file << cnt; + for(uint i = 0; i < cnt; ++i) { + budget.saveAlloc(file, allocations[i]); + allocations[i].save(file); + } + } + + void load(Budget& budget, SaveFile& file) { + file >> spent; + file >> remaining; + file >> gainedMaintenance; + file >> remainingMaintenance; + + uint cnt = 0; + file >> cnt; + for(uint i = 0; i < cnt; ++i) { + auto@ alloc = budget.loadAlloc(file); + alloc.load(file); + } + } +}; + +final class Budget : AIComponent { + //Budget thresholds + private int _criticalThreshold = 350; + private int _lowThreshold = 400; + private int _mediumThreshold = 500; + private int _highThreshold = 1000; + private int _veryHighThreshold = 2000; + + //Focus flags + private bool _askedFocus = false; + private bool _focusing = false; + //Focused budget type + private uint _focus; + + int get_criticalThreshold() const { return _criticalThreshold; } + int get_lowThreshold() const { return _lowThreshold; } + int get_mediumThreshold() const { return _mediumThreshold; } + int get_highThreshold() const { return _highThreshold; } + int get_veryHighThreshold() const { return _veryHighThreshold; } + + array parts; + int NextAllocId = 0; + + int InitialBudget = 0; + int InitialUpcoming = 0; + + double Progress = 0; + double RemainingTime = 0; + + int FreeBudget = 0; + int FreeMaintenance = 0; + + bool checkedMilitarySpending = false; + + void create() { + parts.length = BT_COUNT; + for(uint i = 0; i < BT_COUNT; ++i) { + @parts[i] = BudgetPart(); + parts[i].type = BudgetType(i); + } + } + + void save(SaveFile& file) { + file << InitialBudget; + file << InitialUpcoming; + file << Progress; + file << RemainingTime; + file << FreeBudget; + file << FreeMaintenance; + file << NextAllocId; + file << checkedMilitarySpending; + file << _askedFocus; + file << _focusing; + file << _focus; + + for(uint i = 0, cnt = parts.length; i < cnt; ++i) + parts[i].save(this, file); + } + + void load(SaveFile& file) { + file >> InitialBudget; + file >> InitialUpcoming; + file >> Progress; + file >> RemainingTime; + file >> FreeBudget; + file >> FreeMaintenance; + file >> NextAllocId; + file >> checkedMilitarySpending; + file >> _askedFocus; + file >> _focusing; + file >> _focus; + + for(uint i = 0, cnt = parts.length; i < cnt; ++i) + parts[i].load(this, file); + } + + array loadIds; + AllocateBudget@ loadAlloc(int id) { + if(id == -1) + return null; + for(uint i = 0, cnt = loadIds.length; i < cnt; ++i) { + if(loadIds[i].id == id) + return loadIds[i]; + } + AllocateBudget alloc; + alloc.id = id; + loadIds.insertLast(alloc); + return alloc; + } + AllocateBudget@ loadAlloc(SaveFile& file) { + int id = -1; + file >> id; + if(id == -1) + return null; + else + return loadAlloc(id); + } + void saveAlloc(SaveFile& file, AllocateBudget@ alloc) { + int id = -1; + if(alloc !is null) + id = alloc.id; + file << id; + } + void postLoad(AI& ai) { + loadIds.length = 0; + } + + void spend(uint type, int money, int maint = 0) { + auto@ part = parts[type]; + + part.spent += money; + part.gainedMaintenance += maint; + + if(part.remaining >= money) { + part.remaining -= money; + } + else if(part.remaining >= 0) { + money -= part.remaining; + FreeBudget -= money; + part.remaining = 0; + } + else { + FreeBudget -= money; + } + + if(part.remainingMaintenance >= maint) { + part.remainingMaintenance -= maint; + } + else if(part.remainingMaintenance >= 0) { + maint -= part.remainingMaintenance; + FreeMaintenance -= maint; + part.remainingMaintenance = 0; + } + else { + FreeMaintenance -= money; + } + } + + bool canSpend(uint type, int money, int maint = 0, double priority = 1.0) { + int canFree = FreeBudget; + int canFreeMaint = FreeMaintenance; + + if (priority < 2.0) { + //Rules for normal priority requests + //Don't allow any spending not in our current focus + if (_focusing) { + if (type != _focus) + return false; + } + if (type != BT_Colonization + && (maint > 200 && ai.empire.EstNextBudget < mediumThreshold) + || (maint > 100 && ai.empire.EstNextBudget < lowThreshold) + || (maint > 0 && ai.empire.EstNextBudget < criticalThreshold)) + //Don't allow any high maintenance cost if our estimated next budget is too low + return false; + + //Don't allow generic spending until we've checked if we need to spend on military this cycle + if(type == BT_Development && !checkedMilitarySpending && Progress < 0.33) + canFree = 0; + if(type == BT_Colonization) + canFree += 160; + } + else { + //Rules for high priority requests + if (money > FreeBudget && ai.empire.canBorrow(money - FreeBudget)) + //Allow borrowing from next budget for high priority requests + canFree = money; + } + + auto@ part = parts[type]; + if(money > part.remaining + canFree) + return false; + if(maint != 0 && maint > part.remainingMaintenance + canFreeMaint) + return false; + return true; + } + + int spendable(uint type) { + return FreeBudget + parts[type].remaining; + } + + int maintainable(uint type) { + return FreeMaintenance + parts[type].remainingMaintenance; + } + + void claim(uint type, int money, int maint = 0) { + auto@ part = parts[type]; + + FreeBudget -= money; + part.remaining += money; + + FreeMaintenance -= maint; + part.remainingMaintenance += maint; + } + + void turn() { + if(log && gameTime > 10.0) { + ai.print("=============="); + ai.print("Unspent:"); + ai.print(" Military: "+parts[BT_Military].remaining+" / "+parts[BT_Military].remainingMaintenance); + ai.print(" Infrastructure: "+parts[BT_Infrastructure].remaining+" / "+parts[BT_Infrastructure].remainingMaintenance); + ai.print(" Colonization: "+parts[BT_Colonization].remaining+" / "+parts[BT_Colonization].remainingMaintenance); + ai.print(" Development: "+parts[BT_Development].remaining+" / "+parts[BT_Development].remainingMaintenance); + ai.print(" FREE: "+FreeBudget+" / "+FreeMaintenance); + ai.print("=============="); + ai.print("Total Expenditures:"); + ai.print(" Military: "+parts[BT_Military].spent+" / "+parts[BT_Military].gainedMaintenance); + ai.print(" Infrastructure: "+parts[BT_Infrastructure].spent+" / "+parts[BT_Infrastructure].gainedMaintenance); + ai.print(" Colonization: "+parts[BT_Colonization].spent+" / "+parts[BT_Colonization].gainedMaintenance); + ai.print(" Development: "+parts[BT_Development].spent+" / "+parts[BT_Development].gainedMaintenance); + ai.print("=============="); + } + + //Collect some data about this turn + InitialBudget = ai.empire.RemainingBudget; + InitialUpcoming = ai.empire.EstNextBudget; + + FreeBudget = InitialBudget; + FreeMaintenance = InitialUpcoming; + + checkedMilitarySpending = false; + + //Handle focus status + if (_focusing) { + _focusing = false; + if (log) + ai.print("Budget: ending focus"); + } + else if (_askedFocus) { + _focusing = true; + _askedFocus = false; + if (log) + ai.print("Budget: starting focus"); + } + + //Tell the budget parts to perform turns + for(uint i = 0, cnt = parts.length; i < cnt; ++i) + parts[i].turn(ai, this); + } + + void remove(AllocateBudget@ alloc) { + if(alloc is null) + return; + if(alloc.allocated) { + FreeBudget += alloc.cost; + FreeMaintenance += alloc.maintenance; + } + parts[alloc.type].allocations.remove(alloc); + } + + AllocateBudget@ allocate(uint type, int cost, int maint = 0, double priority = 1.0) { + AllocateBudget alloc; + alloc.id = NextAllocId++; + alloc.type = type; + alloc.cost = cost; + alloc.maintenance = maint; + alloc.priority = priority; + + return allocate(alloc); + } + + AllocateBudget@ allocate(AllocateBudget@ allocation) { + allocation.requestTime = gameTime; + parts[allocation.type].allocations.insertLast(allocation); + parts[allocation.type].allocations.sortDesc(); + return allocation; + } + + void applyNow(AllocateBudget@ alloc) { + auto@ part = parts[alloc.type]; + spend(alloc.type, alloc.cost, alloc.maintenance); + alloc.allocated = true; + part.allocations.remove(alloc); + } + + void grantBonus(int cost, int maint = 0) { + //Spread some bonus budget across all the different parts + FreeBudget += cost; + FreeMaintenance += maint; + } + + bool canFocus() { + return !(ai.empire.EstNextBudget <= criticalThreshold || _askedFocus || _focusing); + } + + //Focus spendings on one particular budget part for one turn + //Only high priority requests will be considered for other parts + //Should be called at the start of a turn for best results + void focus(BudgetType type) { + if (ai.empire.EstNextBudget > criticalThreshold && !_askedFocus && !_focusing) { + _focus = type; + //If we are still at the start of a turn, focus immediately, else wait until next turn + //The second condition compensates for slight timing inaccuracies and execution delay + if (Progress < 0.33 || Progress > 0.995) { + _focusing = true; + _askedFocus = false; + if (log) + ai.print("Budget: starting focus"); + } + else + _askedFocus = true; + } + } + + void tick(double time) { + //Record some simple data + Progress = ai.empire.BudgetTimer / ai.empire.BudgetCycle; + RemainingTime = ai.empire.BudgetCycle - ai.empire.BudgetTimer; + + //Update one of the budget parts + for(uint i = 0, cnt = parts.length; i < cnt; ++i) { + auto@ part = parts[i]; + part.update(ai, this); + } + } + + void focusTick(double time) { + //Detect any extra budget we need to use + int ExpectBudget = FreeBudget; + int ExpectMaint = FreeMaintenance; + for(uint i = 0, cnt = parts.length; i < cnt; ++i) { + ExpectBudget += parts[i].remaining; + ExpectMaint += parts[i].remainingMaintenance; + } + + int HaveBudget = ai.empire.RemainingBudget; + int HaveMaint = ai.empire.EstNextBudget; + if(ExpectBudget != HaveBudget || ExpectMaint != HaveMaint) + grantBonus(HaveBudget - ExpectBudget, max(0, HaveMaint - ExpectMaint)); + } +}; + +AIComponent@ createBudget() { + return Budget(); +} ADDED scripts/server/empire_ai/weasel/Colonization.as Index: scripts/server/empire_ai/weasel/Colonization.as ================================================================== --- scripts/server/empire_ai/weasel/Colonization.as +++ scripts/server/empire_ai/weasel/Colonization.as @@ -0,0 +1,1321 @@ +// Colonization +// ------------ +// Deals with colonization for requested resources. +// + +import empire_ai.weasel.WeaselAI; +import empire_ai.weasel.ImportData; +import empire_ai.weasel.Resources; +import empire_ai.weasel.Planets; +import empire_ai.weasel.Systems; +import empire_ai.weasel.Budget; +import empire_ai.weasel.Creeping; + +import util.formatting; + +import systems; + +enum ColonizationPhase { + CP_Expansion, + CP_Stabilization, +}; + +interface RaceColonization { + bool orderColonization(ColonizeData& data, Planet@ sourcePlanet); + double getGenericUsefulness(const ResourceType@ type); +}; + +final class ColonizeData { + int id = -1; + Planet@ target; + Planet@ colonizeFrom; + bool completed = false; + bool canceled = false; + double checkTime = -1.0; + + void save(Colonization& colonization, SaveFile& file) { + file << target; + file << colonizeFrom; + file << completed; + file << canceled; + file << checkTime; + } + + void load(Colonization& colonization, SaveFile& file) { + file >> target; + file >> colonizeFrom; + file >> completed; + file >> canceled; + file >> checkTime; + } +}; + +tidy final class WaitUsed { + ImportData@ forData; + ExportData@ resource; + + void save(Colonization& colonization, SaveFile& file) { + colonization.resources.saveImport(file, forData); + colonization.resources.saveExport(file, resource); + } + + void load(Colonization& colonization, SaveFile& file) { + @forData = colonization.resources.loadImport(file); + @resource = colonization.resources.loadExport(file); + } +}; + +final class ColonizePenalty : Savable { + Planet@ pl; + double until; + + void save(SaveFile& file) { + file << pl; + file << until; + } + + void load(SaveFile& file) { + file >> pl; + file >> until; + } +}; + +final class PotentialColonize { + Planet@ pl; + const ResourceType@ resource; + double weight = 0; +}; + +final class ColonizeLog { + int typeId; + double time; +}; + +tidy final class ColonizeQueue { + ResourceSpec@ spec; + Planet@ target; + ColonizeData@ step; + ImportData@ forData; + ColonizeQueue@ parent; + array children; + + void save(Colonization& colonization, SaveFile& file) { + file << spec; + file << target; + + colonization.saveColonize(file, step); + colonization.resources.saveImport(file, forData); + + uint cnt = children.length; + file << cnt; + for(uint i = 0; i < cnt; ++i) + children[i].save(colonization, file); + } + + void load(Colonization& colonization, SaveFile& file) { + @spec = ResourceSpec(); + file >> spec; + file >> target; + + @step = colonization.loadColonize(file); + @forData = colonization.resources.loadImport(file); + + uint cnt = 0; + file >> cnt; + children.length = cnt; + for(uint i = 0; i < cnt; ++i) { + @children[i] = ColonizeQueue(); + @children[i].parent = this; + children[i].load(colonization, file); + } + } +}; + +final class Colonization : AIComponent { + const ResourceClass@ foodClass, waterClass, scalableClass; + + Resources@ resources; + Planets@ planets; + Systems@ systems; + Budget@ budget; + Creeping@ creeping; + RaceColonization@ race; + + array queue; + array colonizing; + array awaitingSource; + array waiting; + array penalties; + set_int penaltySet; + int nextColonizeId = 0; + array colonizeLog; + + array sources; + double sourceUpdate = 0; + + //Maximum colonizations that can still be done this turn + uint remainColonizations = 0; + //Amount of colonizations that have happened so far this budget cycle + uint curColonizations = 0; + //Amount of colonizations that happened the previous budget cycle + uint prevColonizations = 0; + + //Whether to automatically find sources and order colonizations + bool performColonization = true; + bool queueColonization = true; + + //Colonization focus + private uint _phase = CP_Expansion; + + //Territory request data + private bool _needsMoreTerritory = false; + private bool _needsNewTerritory = false; + private uint _territoryRequests = 0; + private Region@ _newTerritoryTarget; + + Object@ colonizeWeightObj; + + bool get_needsMoreTerritory() const { return _needsMoreTerritory; } + bool get_needsNewTerritory() const { return _needsNewTerritory; } + + void create() { + @resources = cast(ai.resources); + @planets = cast(ai.planets); + @systems = cast(ai.systems); + @budget = cast(ai.budget); + @creeping = cast(ai.creeping); + @race = cast(ai.race); + + //Get some heuristic resource classes + @foodClass = getResourceClass("Food"); + @waterClass = getResourceClass("WaterType"); + @scalableClass = getResourceClass("Scalable"); + + } + + void save(SaveFile& file) { + file << nextColonizeId; + file << remainColonizations; + file << curColonizations; + file << prevColonizations; + file << _phase; + file << _needsMoreTerritory; + file << _needsNewTerritory; + file << _territoryRequests; + if (_newTerritoryTarget !is null) { + file.write1(); + file << _newTerritoryTarget; + } + else + file.write0(); + + uint cnt = colonizing.length; + file << cnt; + for(uint i = 0; i < cnt; ++i) { + saveColonize(file, colonizing[i]); + colonizing[i].save(this, file); + } + + cnt = waiting.length; + file << cnt; + for(uint i = 0; i < cnt; ++i) + waiting[i].save(this, file); + + cnt = penalties.length; + file << cnt; + for(uint i = 0; i < cnt; ++i) + penalties[i].save(file); + + cnt = colonizeLog.length; + file << cnt; + for(uint i = 0; i < cnt; ++i) { + file.writeIdentifier(SI_Resource, colonizeLog[i].typeId); + file << colonizeLog[i].time; + } + + cnt = queue.length; + file << cnt; + for(uint i = 0; i < cnt; ++i) + queue[i].save(this, file); + } + + void load(SaveFile& file) { + file >> nextColonizeId; + file >> remainColonizations; + file >> curColonizations; + file >> prevColonizations; + file >> _phase; + file >> _needsMoreTerritory; + file >> _needsNewTerritory; + file >> _territoryRequests; + if(file.readBit()) { + file >> _newTerritoryTarget; + } + + uint cnt = 0; + file >> cnt; + for(uint i = 0; i < cnt; ++i) { + auto@ data = loadColonize(file); + if(data !is null) { + data.load(this, file); + if(data.target !is null) { + colonizing.insertLast(data); + if(data.colonizeFrom is null) + awaitingSource.insertLast(data); + } + else { + data.canceled = true; + } + } + else { + ColonizeData().load(this, file); + } + } + + file >> cnt; + waiting.length = cnt; + for(uint i = 0; i < cnt; ++i) { + @waiting[i] = WaitUsed(); + waiting[i].load(this, file); + } + + file >> cnt; + for(uint i = 0; i < cnt; ++i) { + ColonizePenalty pen; + pen.load(file); + if(pen.pl !is null) { + penaltySet.insert(pen.pl.id); + penalties.insertLast(pen); + } + } + + file >> cnt; + for(uint i = 0; i < cnt; ++i) { + ColonizeLog logEntry; + logEntry.typeId = file.readIdentifier(SI_Resource); + file >> logEntry.time; + colonizeLog.insertLast(logEntry); + } + + file >> cnt; + queue.length = cnt; + for(uint i = 0; i < cnt; ++i) { + @queue[i] = ColonizeQueue(); + queue[i].load(this, file); + } + } + + array loadIds; + ColonizeData@ loadColonize(int id) { + if(id == -1) + return null; + for(uint i = 0, cnt = loadIds.length; i < cnt; ++i) { + if(loadIds[i].id == id) + return loadIds[i]; + } + ColonizeData data; + data.id = id; + loadIds.insertLast(data); + return data; + } + ColonizeData@ loadColonize(SaveFile& file) { + int id = -1; + file >> id; + if(id == -1) + return null; + else + return loadColonize(id); + } + void saveColonize(SaveFile& file, ColonizeData@ data) { + int id = -1; + if(data !is null) + id = data.id; + file << id; + } + void postLoad(AI& ai) { + loadIds.length = 0; + } + + bool canBeColonized(Planet& target) { + if(!target.valid) + return false; + if(target.owner.valid) + return false; + return true; + } + + bool canColonize(Planet& source) { + if(source.level == 0) + return false; + if(source.owner !is ai.empire) + return false; + return true; + } + + bool shouldForceExpansion() { + uint otherColonizedSystems = 0; + for (uint i = 0, cnt = systems.outsideBorder.length; i < cnt; ++i) { + auto@ sys = systems.outsideBorder[i]; + //Check if any system in our tradable area is unexplored + if (!sys.explored) + return false; + if (sys.planets.length > 0) { + uint otherColonizedPlanets = 0; + for (uint j = 0, cnt = sys.planets.length; j < cnt; ++j) { + auto@ pl = sys.planets[j]; + int resId = pl.primaryResourceType; + if (resId != -1) { + //Check if any planet can still be colonized in our tradable area + if (!pl.owner.valid && !pl.quarantined) + return false; + else + ++otherColonizedPlanets; + } + } + //Check if all planets in the system are colonized + if (otherColonizedPlanets == sys.planets.length) + ++otherColonizedSystems; + } + } + //Check if all systems in our tradable area belong to other empires + if (otherColonizedSystems == systems.outsideBorder.length) + //If 0, we colonized everything! + return false; + + return true; + } + + double getSourceWeight(PotentialSource& source, ColonizeData& data) { + double w = source.weight; + w /= data.target.position.distanceTo(source.pl.position); + return w; + } + + void updateSources() { + planets.getColonizeSources(sources); + } + + void focusTick(double time) { + if(sourceUpdate < gameTime && performColonization) { + updateSources(); + if(sources.length == 0 && gameTime < 60.0) + sourceUpdate = gameTime + 1.0; + else + sourceUpdate = gameTime + 10.0; + } + + if (ai.behavior.forbidColonization) return; + + //Find some new colonizations we can queue up from resources + fillQueueFromRequests(); + + //If we've gained any requests, see if we can order another colonize + if(remainColonizations > 0 + && (budget.Progress < ai.behavior.colonizeMaxBudgetProgress || gameTime < 3.0 * 60.0) + && (sources.length > 0 || !performColonization) && canColonize() + && queueColonization) { + //Actually go order some colonizations from the queue + if(orderFromQueue()) { + doColonize(); + } + else if(awaitingSource.length == 0) { + if(genericExpand() !is null) + doColonize(); + } + } + + //Find colonization sources for everything that needs them + if(awaitingSource.length != 0 && performColonization) { + for(uint i = 0, cnt = awaitingSource.length; i < cnt; ++i) { + auto@ target = awaitingSource[i]; + + PotentialSource@ src; + double bestSource = 0; + + for(uint j = 0, jcnt = sources.length; j < jcnt; ++j) { + double w = getSourceWeight(sources[j], target); + if(w > bestSource) { + bestSource = w; + @src = sources[j]; + } + } + + if(src !is null) { + orderColonization(target, src.pl); + sources.remove(src); + --i; --cnt; + } + } + } + + //Check if any resources we're waiting for are being used + for(uint i = 0, cnt = waiting.length; i < cnt; ++i) { + auto@ wait = waiting[i]; + if(wait.resource.obj is null || !wait.resource.obj.valid || wait.resource.obj.owner !is ai.empire || wait.resource.request !is null) { + wait.forData.isColonizing = false; + waiting.removeAt(i); + --i; --cnt; + } + } + + //Prune old colonization penalties + for(uint i = 0, cnt = penalties.length; i < cnt; ++i) { + auto@ pen = penalties[i]; + if(pen.pl !is null && pen.pl.owner is ai.empire) + pen.pl.forceAbandon(); + if(pen.until < gameTime) { + if(pen.pl !is null) + penaltySet.erase(pen.pl.id); + penalties.removeAt(i); + --i; --cnt; + } + } + } + + void orderColonization(ColonizeData& data, Planet& sourcePlanet) { + if(log) + ai.print("start colonizing "+data.target.name, sourcePlanet); + + if(race !is null) { + if(race.orderColonization(data, sourcePlanet)) + return; + } + + @data.colonizeFrom = sourcePlanet; + awaitingSource.remove(data); + + sourcePlanet.colonize(data.target); + } + + void tick(double time) { + //Check if we've finished colonizing anything + for(uint i = 0, cnt = colonizing.length; i < cnt; ++i) { + auto@ c = colonizing[i]; + + //Remove if we can no longer colonize it + Empire@ visOwner = c.target.visibleOwnerToEmp(ai.empire); + if(visOwner !is ai.empire && (visOwner is null || visOwner.valid)) { + //Fail out this colonization + cancelColonization(c); + --i; --cnt; + continue; + } + + //Check for succesful colonization + if(visOwner is ai.empire) { + double population = c.target.population; + if(population >= 1.0) { + finishColonization(c); + colonizing.removeAt(i); + --i; --cnt; + continue; + } + else { + if(c.checkTime == -1.0) { + c.checkTime = gameTime; + } + else { + double grace = ai.behavior.colonizeFailGraceTime; + if(population > 0.9) + grace *= 2.0; + if(c.checkTime + grace < gameTime) { + //Fail out this colonization and penalize the target + creeping.requestClear(systems.getAI(c.target.region)); + cancelColonization(c, penalize=ai.behavior.colonizePenalizeTime); + --i; --cnt; + continue; + } + } + } + } + + //This colonization is still waiting for a good source + if(c.colonizeFrom is null) + continue; + + //Check for failed colonization + if(!canColonize(c.colonizeFrom) || !performColonization) { + if(c.target.owner is ai.empire && performColonization) + c.target.stopColonizing(c.target); + + @c.colonizeFrom = null; + awaitingSource.insertAt(0, c); + } + } + + //Update the colonization queue + updateQueue(); + } + + void cancelColonization(ColonizeData@ data, double penalize = 0) { + if(data.colonizeFrom !is null && data.colonizeFrom.owner is ai.empire) + data.colonizeFrom.stopColonizing(data.target); + if(data.colonizeFrom is null) + awaitingSource.remove(data); + if(data.target.owner is ai.empire) + data.target.forceAbandon(); + data.canceled = true; + sourceUpdate = 0; + colonizing.remove(data); + + if(penalize != 0) { + ColonizePenalty pen; + @pen.pl = data.target; + pen.until = gameTime + penalize; + + penaltySet.insert(pen.pl.id); + penalties.insertLast(pen); + } + } + + void finishColonization(ColonizeData@ data) { + if(data.colonizeFrom is null) + awaitingSource.remove(data); + + //If we just colonized a new territory, reset request data + if (data.target.region is _newTerritoryTarget) { + _needsNewTerritory = false; + @_newTerritoryTarget = null; + } + + PlanetAI@ plAI = planets.register(data.target); + + ColonizeLog logEntry; + logEntry.typeId = data.target.primaryResourceType; + logEntry.time = gameTime; + colonizeLog.insertLast(logEntry); + + data.completed = true; + sourceUpdate = 0; + } + + double timeSinceMatchingColonize(ResourceSpec& spec) { + for(int i = colonizeLog.length - 1; i >= 0; --i) { + auto@ res = getResource(colonizeLog[i].typeId); + if(res !is null && spec.meets(res)) + return gameTime - colonizeLog[i].time; + } + return gameTime; + } + + bool isColonizing(Planet& pl) { + for(uint i = 0, cnt = colonizing.length; i < cnt; ++i) { + if(colonizing[i].target is pl) + return true; + } + for(uint i = 0, cnt = queue.length; i < cnt; ++i) { + if(isColonizing(pl, queue[i])) + return true; + } + return false; + } + + bool isColonizing(Planet& pl, ColonizeQueue@ q) { + if(q.target is pl) + return true; + for(uint i = 0, cnt = q.children.length; i < cnt; ++i) { + if(isColonizing(pl, q.children[i])) + return true; + } + return false; + } + + double getGenericUsefulness(const ResourceType@ type) { + //Return a relative value for colonizing the resource this planet has in a vacuum, + //rather than as an explicit requirement for a planet. + double weight = 1.0; + if(type.level == 0) { + weight *= 2.0; + } + else { + weight /= sqr(double(1 + type.level)); + weight *= 0.001; + } + if(type.cls is foodClass || type.cls is waterClass) + weight *= 10.0; + if(type.cls is scalableClass) + weight *= 0.0001; + if(type.totalPressure > 0) + weight *= double(type.totalPressure); + if(race !is null) + weight *= race.getGenericUsefulness(type); + return weight; + } + + ColonizeData@ colonize(Planet& pl) { + if(log) + ai.print("queue colonization", pl); + + ColonizeData data; + data.id = nextColonizeId++; + @data.target = pl; + + budget.spend(BT_Colonization, 0, ai.behavior.colonizeBudgetCost); + + colonizing.insertLast(data); + awaitingSource.insertLast(data); + return data; + } + + ColonizeData@ colonize(ResourceSpec@ spec) { + Planet@ newColony; + double w; + double bestWeight = 0.0; + + for(uint i = 0, cnt = potentials.length; i < cnt; ++i) { + auto@ p = potentials[i]; + + Region@ reg = p.pl.region; + if(reg is null) + continue; + if(!spec.meets(p.resource)) + continue; + if(isColonizing(p.pl)) + continue; + //Skip planets out of our new territory target if we are colonizing a new one + if (_newTerritoryTarget !is null && p.pl.region !is _newTerritoryTarget) + continue; + + auto@ sys = systems.getAI(reg); + w = 1.0; + if (sys.border) + w *= 0.25; + if (!sys.owned && !sys.border) + w /= 0.25; + if (sys.obj.PlanetsMask & ~ai.mask != 0) + w *= 0.25; + if (w > bestWeight) { + @newColony = p.pl; + bestWeight = w; + } + } + + if(newColony !is null) + return colonize(newColony); + else + return null; + } + + array potentials; + void checkSystem(SystemAI@ sys) { + uint presentMask = sys.seenPresent; + if(presentMask & ai.mask == 0) { + if(!ai.behavior.colonizeEnemySystems && (presentMask & ai.enemyMask) != 0) + return; + if(!ai.behavior.colonizeNeutralOwnedSystems && (presentMask & ai.neutralMask) != 0) + return; + if(!ai.behavior.colonizeAllySystems && (presentMask & ai.allyMask) != 0) + return; + } + + double sysWeight = 1.0; + if(presentMask & ai.mask == 0) + sysWeight *= ai.behavior.weightOutwardExpand; + + uint plCnt = sys.planets.length; + for(uint n = 0; n < plCnt; ++n) { + Planet@ pl = sys.planets[n]; + Empire@ visOwner = pl.visibleOwnerToEmp(ai.empire); + if(!pl.valid || visOwner.valid) + continue; + if(isColonizing(pl)) + continue; + if(penaltySet.contains(pl.id)) + continue; + if(pl.quarantined) + continue; + + int resId = pl.primaryResourceType; + if(resId == -1) + continue; + + PotentialColonize p; + @p.pl = pl; + @p.resource = getResource(resId); + p.weight = 1.0 * sysWeight; + //TODO: this should be weighted according to the position of the planet, + //we should try to colonize things in favorable positions + potentials.insertLast(p); + } + } + + double nextPotentialCheck = 0.0; + array@ getPotentialColonize() { + if(gameTime < nextPotentialCheck) + return potentials; + + potentials.length = 0; + for(uint i = 0, cnt = systems.owned.length; i < cnt; ++i) + checkSystem(systems.owned[i]); + for(uint i = 0, cnt = systems.outsideBorder.length; i < cnt; ++i) + checkSystem(systems.outsideBorder[i]); + + if(needsNewTerritory) { + for(uint i = 0, cnt = systems.all.length; i < cnt; ++i) { + if(systems.all[i].explored) + checkSystem(systems.all[i]); + } + } + + if(systems.owned.length == 0) { + Region@ homeSys = ai.empire.HomeSystem; + if(homeSys !is null) { + auto@ homeAI = systems.getAI(homeSys); + if(homeAI !is null) + checkSystem(homeAI); + } + else { + for(uint i = 0, cnt = systems.all.length; i < cnt; ++i) { + if(systems.all[i].visible) + checkSystem(systems.all[i]); + } + } + } + + if(potentials.length == 0 && gameTime < 60.0) + nextPotentialCheck = gameTime + 1.0; + else + nextPotentialCheck = gameTime + randomd(10.0, 40.0); + + //TODO: This should be able to colonize across empires we have trade agreements with? + return potentials; + } + + bool canColonize() { + if(remainColonizations == 0) + return false; + if(curColonizations >= ai.behavior.guaranteeColonizations) { + if(!budget.canSpend(BT_Colonization, 0, ai.behavior.colonizeBudgetCost)) + return false; + } + if(ai.behavior.maxConcurrentColonizations <= colonizing.length) + return false; + return true; + } + + void doColonize() { + remainColonizations -= 1; + curColonizations += 1; + budget.spend(BT_Colonization, 0, ai.behavior.colonizeBudgetCost); + } + + ColonizeData@ genericExpand() { + auto@ potentials = getPotentialColonize(); + + //Do generic expansion using any remaining colonization steps we have + if(ai.behavior.colonizeGenericExpand) { + PotentialColonize@ expand; + double w; + double bestWeight = 0.0; + + for(uint i = 0, cnt = potentials.length; i < cnt; ++i) { + auto@ p = potentials[i]; + w = p.weight * getGenericUsefulness(p.resource); + modPotentialWeight(p, w); + + Region@ reg = p.pl.region; + if(reg is null) + continue; + if(reg.PlanetsMask & ai.mask != 0) + continue; + //Skip planets out of our new territory target if we are colonizing a new one + if (_newTerritoryTarget !is null && p.pl.region !is _newTerritoryTarget) + continue; + if(w == 0) + continue; + if (w > bestWeight) { + @expand = p; + bestWeight = w; + } + } + + if(expand !is null) { + auto@ data = colonize(expand.pl); + potentials.remove(expand); + if (needsNewTerritory && _newTerritoryTarget is null) { + //Check if our target planet is outside our tradable area + bool found = false; + for (uint i = 0, cnt = systems.owned.length; i < cnt; ++i) { + if (systems.owned[i].obj is expand.pl.region) + found = true; + } + for (uint i = 0, cnt = systems.outsideBorder.length; i < cnt; ++i) { + if (systems.outsideBorder[i].obj is expand.pl.region) + found = true; + } + if (!found) + @_newTerritoryTarget = expand.pl.region; + } + return data; + } + } + return null; + } + + void turn() { + //Figure out how much we can colonize + remainColonizations = ai.behavior.maxColonizations; + + //Decide colonization phase + if (_phase == CP_Expansion) { + if (ai.empire.EstNextBudget < budget.criticalThreshold) { + remainColonizations = 0; + _phase = CP_Stabilization; + if (log) + ai.print("Colonization: entering stabilization phase with estimated next budget: " + ai.empire.EstNextBudget); + } + else if (ai.empire.EstNextBudget < budget.lowThreshold) { + remainColonizations = 1; + if (log) + ai.print("Colonization: continuing expansion phase with estimated next budget: " + ai.empire.EstNextBudget); + } + else if (ai.empire.EstNextBudget < budget.mediumThreshold) { + remainColonizations = min(2, ai.behavior.maxColonizations); + if (log) + ai.print("Colonization: continuing expansion phase with estimated next budget: " + ai.empire.EstNextBudget); + } + } + else if (_phase == CP_Stabilization) { + if (ai.empire.RemainingBudget > budget.mediumThreshold) { + _phase = CP_Expansion; + if (log) + ai.print("Colonization: entering expansion phase with budget: " + ai.empire.RemainingBudget); + } + else { + remainColonizations = 1; + if (log) + ai.print("Colonization: continuing stabilization phase with budget: " + ai.empire.RemainingBudget); + } + } + + if (ai.empire.EstNextBudget <= 0 && !ai.behavior.forbidScuttle) { + //We are in trouble. Abandon planets sucking budget up + if (log) + ai.print("Colonization: negative budget, abandoning planets"); + auto@ homeworld = ai.empire.Homeworld; + for (uint i = 0, cnt = planets.planets.length; i < cnt; i++) { + auto@ pl = planets.planets[i].obj; + if (pl is homeworld) + continue; + int resId = pl.primaryResourceType; + if(resId == -1) + continue; + const ResourceType@ type = getResource(resId); + if ((type.cls is scalableClass || type.level > 0) && pl.resourceLevel == 0) { + pl.forceAbandon(); + } + } + //If we are still in trouble, abandon more planets + if (ai.empire.EstNextBudget <= 0) { + for (uint i = 0, cnt = planets.planets.length; i < cnt; i++) { + auto@ pl = planets.planets[i].obj; + if (pl is homeworld) + continue; + int resId = pl.primaryResourceType; + if(resId == -1) + continue; + const ResourceType@ type = getResource(resId); + if ((type.cls is foodClass || type.cls is waterClass) && !pl.primaryResourceExported) + pl.forceAbandon(); + } + //More! + if (ai.empire.EstNextBudget <= 0) { + for (uint i = 0, cnt = planets.planets.length; i < cnt; i++) { + auto@ pl = planets.planets[i].obj; + if (pl is homeworld) + continue; + int resId = pl.primaryResourceType; + if(resId == -1) + continue; + const ResourceType@ type = getResource(resId); + if (!(type.cls is foodClass || type.cls is waterClass || type.cls is scalableClass) && type.level == 0) + pl.forceAbandon(); + } + } + } + } + + //Check if we need to push for territory + if (shouldForceExpansion()) { + //If we already spent at least three turns trying to extend our territory, colonize a new one + if (needsMoreTerritory && _territoryRequests >= 3) + _needsNewTerritory = true; + else { + _needsMoreTerritory = true; + _territoryRequests++; + } + } + else { + _needsMoreTerritory = false; + _needsNewTerritory = false; + _territoryRequests = 0; + @_newTerritoryTarget = null; + } + + prevColonizations = curColonizations; + curColonizations = 0; + + updateSources(); + + if(log) { + ai.print("Empire colonization standings at "+formatGameTime(gameTime)+":"); + for(uint i = 0, cnt = getEmpireCount(); i < cnt; ++i) { + Empire@ other = getEmpire(i); + if(other.major) + ai.print(" "+ai.pad(other.name, 20)+" - "+ai.pad(other.TotalPlanets.value+" planets", 15)+" - "+other.points.value+" points"); + } + } + } + + bool shouldQueueFor(const ResourceSpec@ spec, ColonizeQueue@ inside = null) { + auto@ list = inside is null ? this.queue : inside.children; + for(uint i = 0, cnt = list.length; i < cnt; ++i) { + auto@ q = list[i]; + + //haven't managed to resolve it fully, skip it as well + if(spec.type == RST_Level_Specific) { + if(q.spec.type == RST_Level_Specific && q.spec.level == spec.level) { + if(!isResolved(q)) + return false; + } + } + + //Check anything inner to this tree element + if(!shouldQueueFor(spec, q)) + return false; + } + + return true; + } + + bool shouldQueueFor(ImportData@ imp, ColonizeQueue@ inside = null) { + auto@ list = inside is null ? this.queue : inside.children; + for(uint i = 0, cnt = list.length; i < cnt; ++i) { + auto@ q = list[i]; + + //If we already have this in our queue tree, don't colonize it again + if(imp.forLevel) { + if(q.forData is imp) + return false; + if(q.parent !is null && q.parent.step !is null && q.parent.step.target is imp.obj) { + if(q.spec == imp.spec) + return false; + } + } + + //If we're already trying to get something of this level, but we + //haven't managed to resolve it fully, skip it as well + if(imp.spec.type == RST_Level_Specific) { + if(q.spec.type == RST_Level_Specific && q.spec.level == imp.spec.level) { + if(!isResolved(q)) + return false; + } + } + + //Check anything inner to this tree element + if(!shouldQueueFor(imp, q)) + return false; + } + + return true; + } + + ColonizeQueue@ queueColonize(ResourceSpec& spec, bool place = true) { + ColonizeQueue q; + @q.spec = spec; + + if(place) + queue.insertLast(q); + return q; + } + + bool unresolvedInQueue() { + for(uint i = 0, cnt = queue.length; i < cnt; ++i) { + auto@ q = queue[i]; + if(q.parent !is null) + continue; + if(!isResolved(q)) + return true; + } + return false; + } + + bool isResolved(ColonizeQueue@ q) { + if(q.step is null || q.step.canceled) + return false; + for(uint i = 0 , cnt = q.children.length; i < cnt; ++i) { + if(!isResolved(q.children[i])) + return false; + } + return true; + } + + bool isResolved(ImportData@ req, ColonizeQueue@ inside = null) { + auto@ list = inside is null ? this.queue : inside.children; + for(uint i = 0, cnt = list.length; i < cnt; ++i) { + auto@ q = list[i]; + if(q.forData is req) + return isResolved(q); + if(isResolved(req, inside=q)) + return true; + } + return false; + } + + Planet@ resolve(ColonizeQueue@ q) { + if(q.step !is null) + return q.step.target; + + auto@ potentials = getPotentialColonize(); + PotentialColonize@ take; + double takeWeight = 0.0; + + for(uint i = 0, cnt = potentials.length; i < cnt; ++i) { + auto@ p = potentials[i]; + if(!q.spec.meets(p.resource)) + continue; + + //Skip planets out of our new territory target if we are colonizing a new one + if (_newTerritoryTarget !is null && p.pl.region !is _newTerritoryTarget) + continue; + + if(p.weight > takeWeight) { + takeWeight = p.weight; + @take = p; + } + } + + if(take !is null) { + @q.target = take.pl; + potentials.remove(take); + + array allReqs; + for(uint i = 1, cnt = take.resource.level; i <= cnt; ++i) { + const PlanetLevel@ lvl = getPlanetLevel(take.pl, i); + if(lvl !is null) { + array reqList; + array curReqs; + curReqs = allReqs; + + const ResourceRequirements@ reqs = lvl.reqs; + for(uint i = 0, cnt = reqs.reqs.length; i < cnt; ++i) { + auto@ need = reqs.reqs[i]; + + bool found = false; + for(uint n = 0, ncnt = curReqs.length; n < ncnt; ++n) { + if(curReqs[n].implements(need)) { + found = true; + curReqs.removeAt(n); + break; + } + } + + if(!found) + reqList.insertLast(implementSpec(need)); + } + + reqList.sortDesc(); + + auto@ resRace = cast(race); + if(resRace !is null) + resRace.levelRequirements(take.pl, i, reqList); + + for(uint i = 0, cnt = reqList.length; i < cnt; ++i) { + auto@ spec = reqList[i]; + allReqs.insertLast(spec); + + auto@ inner = queueColonize(spec, place=false); + + @inner.parent = q; + q.children.insertLast(inner); + + resolve(inner); + } + } + } + + return take.pl; + } + + return null; + } + + void kill(ColonizeQueue@ q) { + for(uint i = 0, cnt = q.children.length; i < cnt; ++i) + kill(q.children[i]); + q.children.length = 0; + if(q.forData !is null) + q.forData.isColonizing = false; + @q.parent = null; + } + + void modPotentialWeight(PotentialColonize@ c, double& weight) { + if(colonizeWeightObj !is null) + weight /= c.pl.position.distanceTo(colonizeWeightObj.position)/1000.0; + } + + bool update(ColonizeQueue@ q) { + //See if we can find a matching import request + if(q.forData is null && q.parent !is null && q.parent.target !is null) { + for(uint i = 0, cnt = resources.requested.length; i < cnt; ++i) { + auto@ req = resources.requested[i]; + if(req.isColonizing) + continue; + if(req.obj !is q.parent.target) + continue; + if(req.spec != q.spec) + continue; + + req.isColonizing = true; + @q.forData = req; + } + } + + //Cancel everything if our request is already being met + if(q.forData !is null && q.forData.beingMet) { + kill(q); + return false; + } + + //If it's not resolved, try to resolve it + if(q.target is null) + resolve(q); + + //If the colonization failed, try to find a new planet for it + if((q.step !is null && q.step.canceled) || (q.step is null && q.target !is null && !canBeColonized(q.target))) { + auto@ potentials = getPotentialColonize(); + PotentialColonize@ take; + double takeWeight = 0.0; + + for(uint i = 0, cnt = potentials.length; i < cnt; ++i) { + auto@ p = potentials[i]; + if(!q.spec.meets(p.resource)) + continue; + + //Skip planets out of our new territory target if we are colonizing a new one + if (_newTerritoryTarget !is null && p.pl.region !is _newTerritoryTarget) + continue; + + double w = p.weight; + modPotentialWeight(p, w); + + if(w > takeWeight) { + takeWeight = p.weight; + @take = p; + } + } + + if(take !is null) { + @q.target = take.pl; + @q.step = null; + potentials.remove(take); + } + } + + for(uint i = 0, cnt = q.children.length; i < cnt; ++i) { + if(!update(q.children[i])) { + @q.children[i].parent = null; + q.children.removeAt(i); + --i; --cnt; + } + } + + if(q.children.length == 0 && q.step !is null && q.step.completed) { + if(q.forData !is null) { + q.forData.isColonizing = false; + + PlanetAI@ plAI = planets.getAI(q.target); + if(plAI !is null) { + if(plAI.resources.length != 0) { + WaitUsed wait; + @wait.forData = q.forData; + @wait.resource = plAI.resources[0]; + waiting.insertLast(wait); + q.forData.isColonizing = true; + } + } + } + return false; + } + return true; + } + + void updateQueue() { + for(uint i = 0, cnt = queue.length; i < cnt; ++i) { + auto@ q = queue[i]; + if(!update(q)) { + queue.removeAt(i); + --i; --cnt; + } + } + } + + bool orderFromQueue(ColonizeQueue@ inside = null) { + auto@ list = inside is null ? this.queue : inside.children; + for(uint i = 0, cnt = list.length; i < cnt; ++i) { + auto@ q = list[i]; + if(q.step is null && q.target !is null) { + @q.step = colonize(q.target); + return true; + } + + if(orderFromQueue(q)) + return true; + } + return false; + } + + void dumpQueue(ColonizeQueue@ inside = null) { + auto@ list = inside is null ? this.queue : inside.children; + + string prefix = ""; + if(inside !is null) { + prefix += " "; + ColonizeQueue@ top = inside.parent; + while(top !is null) { + prefix += " "; + @top = top.parent; + } + } + + for(uint i = 0, cnt = list.length; i < cnt; ++i) { + auto@ q = list[i]; + + string txt = "- "+q.spec.dump(); + if(q.forData !is null) + txt += " for request "+q.forData.obj.name+""; + else if(q.parent !is null && q.parent.target !is null) + txt += " for parent "+q.parent.target.name+""; + if(q.target !is null) + txt += " ==> "+q.target.name; + print(prefix+txt); + + dumpQueue(q); + } + } + + void fillQueueFromRequests() { + for(uint i = 0, cnt = resources.requested.length; i < cnt && remainColonizations > 0; ++i) { + auto@ req = resources.requested[i]; + if(!req.isOpen) + continue; + if(!req.cycled) + continue; + if(req.claimedFor) + continue; + if(req.isColonizing) + continue; + + if(shouldQueueFor(req)) { + auto@ q = queueColonize(req.spec); + @q.forData = req; + req.isColonizing = true; + } + } + } + +}; + +AIComponent@ createColonization() { + return Colonization(); +} ADDED scripts/server/empire_ai/weasel/Consider.as Index: scripts/server/empire_ai/weasel/Consider.as ================================================================== --- scripts/server/empire_ai/weasel/Consider.as +++ scripts/server/empire_ai/weasel/Consider.as @@ -0,0 +1,447 @@ +// Consider +// -------- +// Helps AI usage hints to consider various things in the empire. +// + +import empire_ai.weasel.WeaselAI; +import empire_ai.weasel.Systems; +import empire_ai.weasel.Planets; +import empire_ai.weasel.Development; +import empire_ai.weasel.Construction; +import empire_ai.weasel.Fleets; +import empire_ai.weasel.Resources; +import empire_ai.weasel.Colonization; +import empire_ai.weasel.Intelligence; + +import buildings; +import ai.consider; + +from ai.artifacts import ArtifactConsider; +from orbitals import OrbitalModule; + +class Consider : AIComponent, Considerer { + Systems@ systems; + Fleets@ fleets; + Planets@ planets; + Construction@ construction; + Development@ development; + Resources@ resources; + Intelligence@ intelligence; + Colonization@ colonization; + + void create() { + @systems = cast(ai.systems); + @fleets = cast(ai.fleets); + @planets = cast(ai.planets); + @development = cast(ai.development); + @construction = cast(ai.construction); + @resources = cast(ai.resources); + @intelligence = cast(ai.intelligence); + @colonization = cast(ai.colonization); + } + + Empire@ get_empire() { + return ai.empire; + } + + Object@ secondary; + ArtifactConsider@ artifactConsider; + double bestWeight; + ImportData@ request; + const BuildingType@ bldType; + const OrbitalModule@ _module; + ConsiderComponent@ comp; + ConsiderFilter@ cfilter; + + double get_selectedWeight() { + return bestWeight; + } + + Object@ get_currentSupplier() { + return secondary; + } + + ArtifactConsider@ get_artifact() { + return artifactConsider; + } + + void set_artifact(ArtifactConsider@ cons) { + @artifactConsider = cons; + } + + double get_idleTime() { + if(request !is null) + return gameTime - request.idleSince; + return 0.0; + } + + double timeSinceMatchingColonize() { + if(request is null) + return INFINITY; + return colonization.timeSinceMatchingColonize(request.spec); + } + + const BuildingType@ get_building() { + return bldType; + } + + void set_building(const BuildingType@ type) { + @bldType = type; + } + + const OrbitalModule@ get_module() { + return _module; + } + + void set_module(const OrbitalModule@ type) { + @_module = type; + } + + ConsiderComponent@ get_component() { + return comp; + } + + void set_component(ConsiderComponent@ comp) { + @this.comp = comp; + } + + void set_filter(ConsiderFilter@ filter) { + @this.cfilter = filter; + } + + void clear() { + @secondary = null; + @artifactConsider = null; + @request = null; + @comp = null; + @bldType = null; + @cfilter = null; + } + + Object@ OwnedSystems(const ConsiderHook& hook, uint limit = uint(-1)) { + Object@ best; + bestWeight = 0.0; + + uint offset = randomi(0, systems.owned.length-1); + uint cnt = min(systems.owned.length, limit); + for(uint i = 0; i < cnt; ++i) { + uint index = (i+offset) % systems.owned.length; + Region@ obj = systems.owned[index].obj; + if(obj !is null) { + if(cfilter !is null && !cfilter.filter(obj)) + continue; + double w = hook.consider(this, obj); + if(w > bestWeight) { + bestWeight = w; + @best = obj; + } + } + } + + clear(); + return best; + } + + Object@ Fleets(const ConsiderHook& hook) { + Object@ best; + bestWeight = 0.0; + for(uint i = 0, cnt = fleets.fleets.length; i < cnt; ++i) { + Object@ fleet = fleets.fleets[i].obj; + if(fleet !is null) { + if(cfilter !is null && !cfilter.filter(fleet)) + continue; + double w = hook.consider(this, fleet); + if(w > bestWeight) { + bestWeight = w; + @best = fleet; + } + } + } + + clear(); + return best; + } + + Object@ BorderSystems(const ConsiderHook& hook) { + Object@ best; + bestWeight = 0.0; + for(uint i = 0, cnt = systems.border.length; i < cnt; ++i) { + Region@ obj = systems.border[i].obj; + if(obj.PlanetsMask & ~ai.mask == 0) + continue; + if(obj !is null) { + if(cfilter !is null && !cfilter.filter(obj)) + continue; + double w = hook.consider(this, obj); + if(w > bestWeight) { + bestWeight = w; + @best = obj; + } + } + } + for(uint i = 0, cnt = systems.outsideBorder.length; i < cnt; ++i) { + Region@ obj = systems.outsideBorder[i].obj; + if(obj.PlanetsMask & ~ai.mask == 0) + continue; + if(obj !is null) { + if(cfilter !is null && !cfilter.filter(obj)) + continue; + double w = hook.consider(this, obj); + if(w > bestWeight) { + bestWeight = w; + @best = obj; + } + } + } + + clear(); + return best; + } + + Object@ OtherSystems(const ConsiderHook& hook) { + Object@ best; + bestWeight = 0.0; + for(uint i = 0, cnt = intelligence.intel.length; i < cnt; ++i) { + auto@ intel = intelligence.intel[i]; + if(intel is null) + continue; + + for(uint i = 0, cnt = intel.theirOwned.length; i < cnt; ++i) { + Region@ obj = intel.theirOwned[i].obj; + if(obj.PlanetsMask & ~ai.mask == 0) + continue; + if(obj !is null) { + if(cfilter !is null && !cfilter.filter(obj)) + continue; + double w = hook.consider(this, obj); + if(w > bestWeight) { + bestWeight = w; + @best = obj; + } + } + } + } + + clear(); + return best; + } + + Object@ SystemsInTerritory(const ConsiderHook& hook, const Territory& territory, uint limit = uint(-1)) { + Object@ best; + bestWeight = 0.0; + + uint regionCount = territory.getRegionCount(); + uint offset = randomi(0, regionCount -1); + uint cnt = min(regionCount, limit); + for(uint i = 0; i < cnt; ++i) { + uint index = (i+offset) % regionCount; + Region@ obj = territory.getRegion(index); + if(obj !is null) { + if(cfilter !is null && !cfilter.filter(obj)) + continue; + double w = hook.consider(this, obj); + if(w > bestWeight) { + bestWeight = w; + @best = obj; + } + } + } + + clear(); + return best; + } + + Object@ ImportantPlanets(const ConsiderHook& hook) { + Object@ best; + bestWeight = 0.0; + for(uint i = 0, cnt = development.focuses.length; i < cnt; ++i) { + Object@ obj = development.focuses[i].obj; + if(obj !is null) { + if(cfilter !is null && !cfilter.filter(obj)) + continue; + double w = hook.consider(this, obj); + if(w > bestWeight) { + bestWeight = w; + @best = obj; + } + } + } + + clear(); + return best; + } + + Object@ ImportantPlanetsInTerritory(const ConsiderHook& hook, const Territory& territory) { + Object@ best;; + bestWeight = 0.0; + for(uint i = 0, cnt = development.focuses.length; i < cnt; ++i) { + Object@ obj = development.focuses[i].obj; + if(obj !is null) { + if(cfilter !is null && !cfilter.filter(obj)) + continue; + if (obj.region !is null) { + if (obj.region.getTerritory(ai.empire) !is territory) + continue; + double w = hook.consider(this, obj); + if(w > bestWeight) { + bestWeight = w; + @best = obj; + } + } + } + } + + clear(); + return best; + } + + Object@ AllPlanets(const ConsiderHook& hook) { + Object@ best; + bestWeight = 0.0; + for(uint i = 0, cnt = planets.planets.length; i < cnt; ++i) { + Object@ obj = planets.planets[i].obj; + if(obj !is null) { + if(cfilter !is null && !cfilter.filter(obj)) + continue; + double w = hook.consider(this, obj); + if(w > bestWeight) { + bestWeight = w; + @best = obj; + } + } + } + + clear(); + return best; + } + + Object@ PlanetsInTerritory(const ConsiderHook& hook, const Territory& territory) { + Object@ best; + bestWeight = 0.0; + for(uint i = 0, cnt = planets.planets.length; i < cnt; ++i) { + Object@ obj = planets.planets[i].obj; + if(obj !is null) { + if(cfilter !is null && !cfilter.filter(obj)) + continue; + if (obj.region !is null) { + if (obj.region.getTerritory(ai.empire) !is territory) + continue; + double w = hook.consider(this, obj); + if(w > bestWeight) { + bestWeight = w; + @best = obj; + } + } + } + } + + clear(); + return best; + } + + Object@ SomePlanets(const ConsiderHook& hook, uint count, bool alwaysImportant) { + Object@ best; + bestWeight = 0.0; + if(alwaysImportant) { + for(uint i = 0, cnt = development.focuses.length; i < cnt; ++i) { + Object@ obj = development.focuses[i].obj; + if(obj !is null) { + if(cfilter !is null && !cfilter.filter(obj)) + continue; + double w = hook.consider(this, obj); + if(w > bestWeight) { + bestWeight = w; + @best = obj; + } + } + } + } + + uint planetCount = planets.planets.length; + uint offset = randomi(0, planetCount-1); + uint cnt = min(count, planetCount); + for(uint i = 0; i < cnt; ++i) { + uint index = (offset+i) % planetCount; + Object@ obj = planets.planets[index].obj; + if(obj !is null) { + if(cfilter !is null && !cfilter.filter(obj)) + continue; + double w = hook.consider(this, obj); + if(w > bestWeight) { + bestWeight = w; + @best = obj; + } + } + } + + clear(); + return best; + } + + Object@ FactoryPlanets(const ConsiderHook& hook) { + Object@ best; + bestWeight = 0.0; + for(uint i = 0, cnt = construction.factories.length; i < cnt; ++i) { + Object@ obj = construction.factories[i].obj; + if(obj !is null && obj.isPlanet) { + if(cfilter !is null && !cfilter.filter(obj)) + continue; + double w = hook.consider(this, obj); + if(w > bestWeight) { + bestWeight = w; + @best = obj; + } + } + } + + clear(); + return best; + } + + Object@ MatchingImportRequests(const ConsiderHook& hook, const ResourceType@ type, bool considerExisting) { + Object@ best; + bestWeight = 0.0; + for(uint i = 0, cnt = resources.requested.length; i < cnt; ++i) { + ImportData@ req = resources.requested[i]; + if(!considerExisting) { + if(req.beingMet || req.claimedFor) + continue; + } + if(req.spec.meets(type, req.obj, req.obj)) { + @secondary = null; + @request = req; + double w = hook.consider(this, req.obj); + if(w > bestWeight) { + if(cfilter !is null && !cfilter.filter(req.obj)) + continue; + bestWeight = w; + @best = req.obj; + } + } + } + if(considerExisting) { + for(uint i = 0, cnt = resources.used.length; i < cnt; ++i) { + ExportData@ res = resources.used[i]; + ImportData@ req = res.request; + if(req !is null && req.spec.meets(type, req.obj, req.obj)) { + @secondary = res.obj; + @request = req; + double w = hook.consider(this, req.obj); + if(w > bestWeight) { + if(cfilter !is null && !cfilter.filter(req.obj)) + continue; + bestWeight = w; + @best = req.obj; + } + } + } + } + + clear(); + return best; + } +}; + +AIComponent@ createConsider() { + return Consider(); +} ADDED scripts/server/empire_ai/weasel/Construction.as Index: scripts/server/empire_ai/weasel/Construction.as ================================================================== --- scripts/server/empire_ai/weasel/Construction.as +++ scripts/server/empire_ai/weasel/Construction.as @@ -0,0 +1,1606 @@ +// Construction +// ------------ +// Manages factories and allows build requests for flagships, orbitals, and +// anything else that requires labor. +// + +import empire_ai.weasel.WeaselAI; +import empire_ai.weasel.Budget; +import empire_ai.weasel.Planets; +import empire_ai.weasel.Designs; +import empire_ai.weasel.Resources; +import empire_ai.weasel.Systems; +import empire_ai.weasel.Orbitals; + +import orbitals; +import saving; + +import systems; +import regions.regions; + +import ai.construction; + +from constructible import ConstructibleType; +from constructions import ConstructionType, getConstructionType; + +class AllocateConstruction : IConstruction { + protected bool _completed = false; + protected bool _started = false; + + protected int _id = -1; + uint moneyType = BT_Development; + Factory@ tryFactory; + double maxTime = INFINITY; + double completedAt = 0; + AllocateBudget@ alloc; + int cost = 0; + int maintenance = 0; + double priority = 1.0; + + AllocateConstruction() { + } + + int id { + get const { return _id; } + set { _id = value; } + } + + bool get_started() const { return _started; } + + bool completed { + get const { return _completed; } + set { _completed = value; } + } + + void _save(Construction& construction, SaveFile& file) { + file << moneyType; + construction.saveFactory(file, tryFactory); + file << maxTime; + file << _completed; + file << _started; + file << completedAt; + construction.budget.saveAlloc(file, alloc); + file << cost; + file << maintenance; + file << priority; + save(construction, file); + } + + void save(Construction& construction, SaveFile& file) { + } + + void _load(Construction& construction, SaveFile& file) { + file >> moneyType; + @tryFactory = construction.loadFactory(file); + file >> maxTime; + file >> _completed; + file >> _started; + file >> completedAt; + @alloc = construction.budget.loadAlloc(file); + file >> cost; + file >> maintenance; + file >> priority; + load(construction, file); + } + + void load(Construction& construction, SaveFile& file) { + } + + bool tick(AI& ai, Construction& construction, double time) { + if(tryFactory !is null && alloc.allocated) { + construction.start(tryFactory, this); + return false; + } + return true; + } + + void update(AI& ai, Factory@ f) { + @alloc = cast(ai.budget).allocate(moneyType, cost, maintenance, priority); + } + + double laborCost(AI& ai, Object@ obj) { + return 0.0; + } + + bool canBuild(AI& ai, Factory@ f) { + return true; + } + + void construct(AI& ai, Factory@ f) { + _started = true; + } + + string toString() { + return "construction"; + } +}; + +class BuildFlagship : AllocateConstruction, IFlagshipConstruction { + protected const Design@ _design; + double baseLabor = 0.0; + DesignTarget@ target; + + BuildFlagship() { + } + + BuildFlagship(const Design@ dsg) { + set(dsg); + } + + BuildFlagship(DesignTarget@ target) { + @this.target = target; + } + + const Design@ get_design() const { return _design; } + + void save(Construction& construction, SaveFile& file) { + file << baseLabor; + if(_design !is null) { + file.write1(); + file << _design; + } + else { + file.write0(); + } + construction.designs.saveDesign(file, target); + } + + void load(Construction& construction, SaveFile& file) { + file >> baseLabor; + if(file.readBit()) + file >> _design; + @target = construction.designs.loadDesign(file); + } + + void set(const Design& dsg) { + @_design = dsg.mostUpdated(); + baseLabor = _design.total(HV_LaborCost); + } + + double laborCost(AI& ai, Object@ obj) { + return baseLabor; + } + + bool tick(AI& ai, Construction& construction, double time) override { + if(target !is null) { + if(target.active !is null) { + set(target.active); + @target = null; + } + } + return AllocateConstruction::tick(ai, construction, time); + } + + bool canBuild(AI& ai, Factory@ f) override { + if(!f.obj.canBuildShips) + return false; + return _design !is null; + } + + void update(AI& ai, Factory@ f) { + double c = _design.total(HV_BuildCost); + c *= double(f.obj.shipBuildCost) / 100.0; + c *= f.obj.constructionCostMod; + + cost = ceil(c); + maintenance = ceil(_design.total(HV_MaintainCost)); + + AllocateConstruction::update(ai, f); + } + + void construct(AI& ai, Factory@ f) { + f.obj.buildFlagship(_design); + AllocateConstruction::construct(ai, f); + } + + string toString() { + if(_design is null) + return "flagship (design in progress)"; + return "flagship " + _design.name; + } +}; + +class BuildFlagshipSourced : BuildFlagship { + Object@ buildAt; + Object@ buildFrom; + + BuildFlagshipSourced() { + } + + BuildFlagshipSourced(const Design@ dsg) { + set(dsg); + } + + BuildFlagshipSourced(DesignTarget@ target) { + @this.target = target; + } + + void save(Construction& construction, SaveFile& file) override { + BuildFlagship::save(construction, file); + file << buildAt; + file << buildFrom; + } + + void load(Construction& construction, SaveFile& file) override { + BuildFlagship::load(construction, file); + file >> buildAt; + file >> buildFrom; + } + + bool canBuild(AI& ai, Factory@ f) override { + if(buildAt !is null && f.obj !is buildAt) + return false; + return BuildFlagship::canBuild(ai, f); + } + + void construct(AI& ai, Factory@ f) override { + f.obj.buildFlagship(_design, constructFrom=buildFrom); + AllocateConstruction::construct(ai, f); + } +}; + +class BuildStation : AllocateConstruction, IStationConstruction { + protected const Design@ _design; + double baseLabor = 0.0; + DesignTarget@ target; + vec3d position; + bool local = false; + + BuildStation() { + } + + BuildStation(const Design@ dsg, const vec3d& position) { + this.position = position; + set(dsg); + } + + BuildStation(DesignTarget@ target, const vec3d& position) { + this.position = position; + @this.target = target; + } + + BuildStation(const Design@ dsg, bool local) { + this.local = true; + set(dsg); + } + + BuildStation(DesignTarget@ target, bool local) { + @this.target = target; + this.local = true; + } + + const Design@ get_design() const { return _design; } + + void save(Construction& construction, SaveFile& file) { + file << baseLabor; + file << position; + file << local; + if(_design !is null) { + file.write1(); + file << _design; + } + else { + file.write0(); + } + construction.designs.saveDesign(file, target); + } + + void load(Construction& construction, SaveFile& file) { + file >> baseLabor; + file >> position; + file >> local; + if(file.readBit()) + file >> _design; + @target = construction.designs.loadDesign(file); + } + + void set(const Design& dsg) { + @_design = dsg.mostUpdated(); + baseLabor = _design.total(HV_LaborCost); + } + + double laborCost(AI& ai, Object@ obj) { + double labor = baseLabor; + + labor *= obj.owner.OrbitalLaborCostFactor; + + if(!local) { + Region@ reg = getRegion(position); + Region@ targReg = obj.region; + if(reg !is null && targReg !is null) { + int hops = cast(ai.systems).tradeDistance(targReg, reg); + if(hops > 0) { + double penalty = 1.0 + config::ORBITAL_LABOR_COST_STEP * double(hops); + baseLabor *= penalty; + } + } + } + return labor; + } + + bool tick(AI& ai, Construction& construction, double time) override { + if(target !is null) { + if(target.active !is null) { + set(target.active); + @target = null; + } + } + return AllocateConstruction::tick(ai, construction, time); + } + + bool canBuild(AI& ai, Factory@ f) override { + if(_design is null) + return false; + if(!f.obj.canBuildOrbitals) + return false; + Region@ targReg = f.obj.region; + if(targReg is null) + return false; + if(!local) { + Region@ reg = getRegion(position); + if(reg is null) + return false; + if(!cast(ai.systems).canTrade(targReg, reg)) + return false; + } + return true; + } + + void update(AI& ai, Factory@ f) { + double c = _design.total(HV_BuildCost); + c *= f.obj.owner.OrbitalBuildCostFactor; + c *= f.obj.constructionCostMod; + + cost = ceil(c); + maintenance = ceil(_design.total(HV_MaintainCost)); + + AllocateConstruction::update(ai, f); + } + + void construct(AI& ai, Factory@ f) { + if(local) { + position = f.obj.position; + vec2d offset = random2d(f.obj.radius + 10.0, f.obj.radius + 100.0); + position.x += offset.x; + position.z += offset.y; + } + f.obj.buildStation(_design, position); + AllocateConstruction::construct(ai, f); + } + + string toString() { + if(_design is null) + return "station (design in progress)"; + return "station " + _design.name; + } +}; + +class BuildOrbital : AllocateConstruction, IOrbitalConstruction { + protected const OrbitalModule@ _module; + double baseLabor = 0.0; + const Planet@ planet; + bool local = false; + vec3d position; + + BuildOrbital() { + } + + BuildOrbital(const OrbitalModule@ module, const vec3d& position) { + this.position = position; + @this._module = module; + baseLabor = module.laborCost; + } + + BuildOrbital(const OrbitalModule@ module, bool local) { + this.local = true; + @this._module = module; + baseLabor = module.laborCost; + } + + BuildOrbital(const OrbitalModule@ module, const Planet@ planet) { + this.local = true; + @this.planet = planet; + @this._module = module; + baseLabor = module.laborCost; + } + + const OrbitalModule@ get_module() const { return _module; } + + void save(Construction& construction, SaveFile& file) { + file << baseLabor; + file << position; + file << local; + file.writeIdentifier(SI_Orbital, _module.id); + } + + void load(Construction& construction, SaveFile& file) { + file >> baseLabor; + file >> position; + file >> local; + @_module = getOrbitalModule(file.readIdentifier(SI_Orbital)); + } + + double laborCost(AI& ai, Object@ obj) { + double labor = baseLabor; + + labor *= obj.owner.OrbitalLaborCostFactor; + + if(!local) { + Region@ reg = getRegion(position); + Region@ targReg = obj.region; + if(reg !is null && targReg !is null) { + int hops = cast(ai.systems).tradeDistance(targReg, reg); + if(hops > 0) { + double penalty = 1.0 + config::ORBITAL_LABOR_COST_STEP * double(hops); + baseLabor *= penalty; + } + } + } + return labor; + } + + bool tick(AI& ai, Construction& construction, double time) override { + return AllocateConstruction::tick(ai, construction, time); + } + + bool canBuild(AI& ai, Factory@ f) override { + if(_module is null) + return false; + if(!f.obj.canBuildOrbitals) + return false; + Region@ targReg = f.obj.region; + if(targReg is null) + return false; + if(!local) { + Region@ reg = getRegion(position); + if(reg is null) + return false; + if(!cast(ai.systems).canTrade(targReg, reg)) + return false; + } + return true; + } + + void update(AI& ai, Factory@ f) { + double c = _module.buildCost; + c *= f.obj.owner.OrbitalBuildCostFactor; + c *= f.obj.constructionCostMod; + + cost = ceil(c); + maintenance = _module.maintenance; + + AllocateConstruction::update(ai, f); + } + + void construct(AI& ai, Factory@ f) { + if(local) { + if (planet !is null) { + position = planet.position; + vec2d offset = random2d(planet.OrbitSize * 0.8, planet.OrbitSize * 0.9); + position.x += offset.x; + position.z += offset.y; + } + else { + position = f.obj.position; + //vec2d offset = random2d(f.obj.radius + 10.0, f.obj.radius + 100.0); + if (f.plAI !is null) { + vec2d offset = random2d(f.plAI.obj.OrbitSize * 0.8, f.plAI.obj.OrbitSize * 0.9); + position.x += offset.x; + position.z += offset.y; + } + } + } + f.obj.buildOrbital(_module.id, position); + AllocateConstruction::construct(ai, f); + } + + string toString() { + return "orbital " + _module.name; + } +}; + +class RetrofitShip : AllocateConstruction { + Ship@ ship; + double labor; + + RetrofitShip() { + } + + RetrofitShip(Ship@ ship) { + @this.ship = ship; + labor = ship.getRetrofitLabor(); + cost = ship.getRetrofitCost(); + } + + void save(Construction& construction, SaveFile& file) { + file << ship; + file << labor; + } + + void load(Construction& construction, SaveFile& file) { + file >> ship; + file >> labor; + } + + double laborCost(AI& ai, Object@ obj) { + return labor; + } + + bool canBuild(AI& ai, Factory@ f) override { + if(!f.obj.canBuildShips) + return false; + Region@ reg = ship.region; + return reg !is null && reg is f.obj.region; + } + + void construct(AI& ai, Factory@ f) { + ship.retrofitFleetAt(f.obj); + AllocateConstruction::construct(ai, f); + } + + string toString() { + return "retrofit "+ship.name; + } +}; + +class BuildConstruction : AllocateConstruction { + const ConstructionType@ consType; + + BuildConstruction() { + } + + BuildConstruction(const ConstructionType@ consType) { + @this.consType = consType; + } + + void save(Construction& construction, SaveFile& file) { + file.writeIdentifier(SI_ConstructionType, consType.id); + } + + void load(Construction& construction, SaveFile& file) { + @consType = getConstructionType(file.readIdentifier(SI_ConstructionType)); + } + + double laborCost(AI& ai, Object@ obj) { + if(obj is null) + return consType.laborCost; + return consType.getLaborCost(obj); + } + + bool canBuild(AI& ai, Factory@ f) override { + return consType.canBuild(f.obj, ignoreCost=true); + } + + void update(AI& ai, Factory@ f) { + cost = consType.getBuildCost(f.obj); + maintenance = consType.getMaintainCost(f.obj); + + AllocateConstruction::update(ai, f); + } + + void construct(AI& ai, Factory@ f) { + f.obj.buildConstruction(consType.id); + AllocateConstruction::construct(ai, f); + } + + string toString() { + return "construction "+consType.name; + } +}; + +class Factory { + Object@ obj; + PlanetAI@ plAI; + + Factory@ exportingTo; + + AllocateConstruction@ active; + double laborAim = 0.0; + double laborIncome = 0.0; + + double idleSince = 0.0; + double storedLabor = 0.0; + double laborMaxStorage = 0.0; + double buildingPenalty = 0.0; + + bool needsSupportLabor = false; + double waitingSupportLabor = 0.0; + uint curConstructionType = 0; + bool valid = true; + bool significantLabor = true; + + uint backgrounded = 0; + Asteroid@ bgAsteroid; + + BuildingRequest@ curBuilding; + ImportData@ curImport; + + void save(Construction& construction, SaveFile& file) { + construction.planets.saveAI(file, plAI); + construction.saveConstruction(file, active); + file << laborAim; + file << laborIncome; + file << idleSince; + file << storedLabor; + file << laborMaxStorage; + file << buildingPenalty; + construction.planets.saveBuildingRequest(file, curBuilding); + construction.resources.saveImport(file, curImport); + file << backgrounded; + file << bgAsteroid; + file << curConstructionType; + file << valid; + file << needsSupportLabor; + file << waitingSupportLabor; + construction.saveFactory(file, exportingTo); + } + + void load(Construction& construction, SaveFile& file) { + @plAI = construction.planets.loadAI(file); + @active = construction.loadConstruction(file); + file >> laborAim; + file >> laborIncome; + file >> idleSince; + file >> storedLabor; + file >> laborMaxStorage; + file >> buildingPenalty; + @curBuilding = construction.planets.loadBuildingRequest(file); + @curImport = construction.resources.loadImport(file); + file >> backgrounded; + file >> bgAsteroid; + file >> curConstructionType; + file >> valid; + file >> needsSupportLabor; + file >> waitingSupportLabor; + @exportingTo = construction.loadFactory(file); + } + + bool get_busy() { + return active !is null; + } + + bool get_needsLabor() { + if(!valid) + return false; + if(obj.hasOrderedSupports) + return true; + if(active !is null) + return true; + if(needsSupportLabor) + return true; + if(obj.constructionCount > 0 && curConstructionType != CT_Export) + return true; + return false; + } + + double laborToBear(AI& ai) { + return laborIncome * ai.behavior.constructionMaxTime + storedLabor; + } + + bool viable(AI& ai, AllocateConstruction@ alloc) { + double labor = obj.laborIncome; + double estTime = (alloc.laborCost(ai, obj) - storedLabor) / labor; + if(estTime > alloc.maxTime) + return false; + return true; + } + + bool tick(AI& ai, Construction& construction, double time) { + if(obj is null || !obj.valid || obj.owner !is ai.empire) { + valid = false; + return false; + } + + if(ai.behavior.forbidConstruction) return true; + + uint curCount = obj.constructionCount; + curConstructionType = 0; + bool isBackground = false; + if(curCount != 0) { + curConstructionType = obj.constructionType; + isBackground = curConstructionType == CT_Asteroid || curConstructionType == CT_Export; + } + if(active !is null) { + if(curCount <= backgrounded || (curCount == 1 && isBackground)) { + if(construction.log) + ai.print("Completed construction of "+active.toString()+" "+backgrounded+" / "+curCount, obj); + active.completed = true; + active.completedAt = gameTime; + @active = null; + idleSince = gameTime; + backgrounded = 0; + } + } + else { + if(curCount < backgrounded) { + backgrounded = curCount; + } + } + + //Background constructibles we don't need to do right now + if(curCount > 1 && curConstructionType == CT_Asteroid && bgAsteroid !is null) { + obj.moveConstruction(obj.constructionID[0], -1); + backgrounded += 1; + } + if(curCount > 1 && curConstructionType == CT_Export && exportingTo !is null) { + obj.cancelConstruction(obj.constructionID[0]); + @exportingTo = null; + } + if(bgAsteroid !is null && (bgAsteroid.owner.valid || curCount == 0)) { + if(bgAsteroid.owner is ai.empire) + construction.planets.register(bgAsteroid); + @bgAsteroid = null; + } + + //Build warehouse(s) if we've been idle + laborIncome = obj.laborIncome; + storedLabor = obj.currentLaborStored; + laborMaxStorage = obj.laborStorageCapacity; + significantLabor = laborIncome >= 0.4 * construction.bestLabor && obj.baseLaborIncome > 4.0/60.0; + if(storedLabor < laborMaxStorage) + idleSince = gameTime; + if(active is null && curBuilding is null && plAI !is null && gameTime - idleSince > ai.behavior.laborStoreIdleTimer && ai.behavior.buildLaborStorage && (laborMaxStorage+50) < ai.behavior.laborStoreMaxFillTime * max(obj.baseLaborIncome, laborAim) && significantLabor) { + auto@ bld = ai.defs.LaborStorage; + if(bld !is null && buildingPenalty < gameTime) { + if(construction.log) + ai.print("Build building "+bld.name+" for labor storage", obj); + + @curBuilding = construction.planets.requestBuilding(plAI, bld); + } + } + + //Remove waits on completed labor gains + if(curBuilding !is null) { + if(curBuilding.canceled || (curBuilding.built && curBuilding.getProgress() >= 1.0)) { + if(construction.log) + ai.print("Building construction for labor finished", obj); + if(curBuilding.canceled) + buildingPenalty = gameTime + 60.0; + @curBuilding = null; + } + } + if(curImport !is null) { + if(curImport.beingMet) { + if(construction.log) + ai.print("Resource import for labor finished", obj); + @curImport = null; + } + } + + //See if we need a new labor gain + if(laborIncome < laborAim) { + if(curImport is null && plAI !is null && obj.isPressureSaturated(TR_Labor) && obj.pressureCap < uint(obj.totalPressure) && gameTime > 6.0 * 60.0 && ai.behavior.buildFactoryForLabor) { + ResourceSpec spec; + spec.type = RST_Pressure_Level0; + spec.pressureType = TR_Labor; + + if(construction.log) + ai.print("Queue resource import for labor", obj); + + @curImport = construction.resources.requestResource(obj, spec, prioritize=true); + } + if(curBuilding is null && plAI !is null && ai.behavior.buildLaborStorage) { + auto@ bld = ai.defs.Factory; + if(bld !is null && buildingPenalty < gameTime) { + if(construction.log) + ai.print("Build building "+bld.name+" for labor", obj); + + @curBuilding = construction.planets.requestBuilding(plAI, bld); + } + } + } + + //See if we should spend our labor on a labor export somewhere else + if(exportingTo !is null && curConstructionType == CT_Export) { + if(!exportingTo.valid || (!exportingTo.needsLabor && exportingTo !is construction.primaryFactory)) { + obj.cancelConstruction(obj.constructionID[0]); + @exportingTo = null; + } + } + if(ai.behavior.distributeLaborExports) { + if(curCount == 0 && obj.canExportLabor) { + uint offset = randomi(0, construction.factories.length-1); + for(uint i = 0, cnt = construction.factories.length; i < cnt; ++i) { + auto@ other = construction.factories[(i+offset) % cnt]; + if(other is this) + continue; + if(!other.obj.canImportLabor) + continue; + + //Check if this is currently busy + if(other !is construction.primaryFactory) { + if(!other.needsLabor) + continue; + } + + obj.exportLaborTo(other.obj); + @exportingTo = other; + } + } + } + + //See if we should spend our labor trying to build an asteroid + if(ai.behavior.backgroundBuildAsteroids) { + if((curCount == 0 || (curConstructionType == CT_Export && curCount == 1)) && storedLabor >= laborMaxStorage * 0.5 && obj.canBuildAsteroids) { + Asteroid@ roid = construction.getBackgroundAsteroid(this); + if(roid !is null) { + uint resCount = roid.getAvailableCount(); + if(resCount != 0) { + uint bestIndex = 0; + int bestId = -1; + double bestWeight = 0.0; + + if(ai.behavior.chooseAsteroidResource) { + for(uint i = 0; i < resCount; ++i) { + int resourceId = roid.getAvailable(i); + double w = asteroidResourceValue(getResource(resourceId)); + if(w > bestWeight) { + bestWeight = w; + bestId = resourceId; + bestIndex = i; + } + } + } + else { + bestIndex = randomi(0, resCount-1); + bestId = roid.getAvailable(bestIndex); + } + + double laborCost = roid.getAvailableCost(bestIndex); + + Region@ fromReg = obj.region; + Region@ toReg = roid.region; + if(fromReg !is null && toReg !is null) + laborCost *= 1.0 + config::ASTEROID_COST_STEP * double(construction.systems.hopDistance(fromReg, toReg)); + + double timeTaken = laborIncome / laborCost; + if(timeTaken < ai.behavior.constructionMaxTime || storedLabor >= laborMaxStorage * 0.95) { + @bgAsteroid = roid; + obj.buildAsteroid(roid, bestId); + + if(construction.log) + ai.print("Use background labor to mine "+roid.name+" in "+roid.region.name, obj); + } + } + } + } + } + + return true; + } + + void aimForLabor(double labor) { + if(labor > laborAim) + laborAim = labor; + } +}; + +class Construction : AIComponent { + array factories; + Factory@ primaryFactory; + double noFactoryTimer = 0.0; + + int nextAllocId = 0; + array allocations; + + double totalLabor = 0.0; + double bestLabor = 0.0; + + BuildOrbital@ buildConsolidate; + + Budget@ budget; + Planets@ planets; + Orbitals@ orbitals; + Resources@ resources; + Designs@ designs; + Systems@ systems; + + void create() { + @budget = cast(ai.budget); + @planets = cast(ai.planets); + @resources = cast(ai.resources); + @designs = cast(ai.designs); + @systems = cast(ai.systems); + @orbitals = cast(ai.orbitals); + } + + void save(SaveFile& file) { + file << nextAllocId; + + uint cnt = allocations.length; + file << cnt; + for(uint i = 0; i < cnt; ++i) + saveConstruction(file, allocations[i]); + + cnt = factories.length; + file << cnt; + for(uint i = 0; i < cnt; ++i) { + saveFactory(file, factories[i]); + factories[i].save(this, file); + } + + saveFactory(file, primaryFactory); + file << noFactoryTimer; + + saveConstruction(file, buildConsolidate); + } + + void load(SaveFile& file) { + file >> nextAllocId; + + uint cnt = 0; + file >> cnt; + for(uint i = 0; i < cnt; ++i) { + auto@ alloc = loadConstruction(file); + if(alloc !is null) + allocations.insertLast(alloc); + } + + file >> cnt; + for(uint i = 0; i < cnt; ++i) { + Factory@ f = loadFactory(file); + if(f !is null) + f.load(this, file); + else + Factory().load(this, file); + } + + @primaryFactory = loadFactory(file); + file >> noFactoryTimer; + + @buildConsolidate = cast(loadConstruction(file)); + } + + void saveFactory(SaveFile& file, Factory@ f) { + if(f !is null) { + file.write1(); + file << f.obj; + } + else { + file.write0(); + } + } + + Factory@ loadFactory(SaveFile& file) { + if(!file.readBit()) + return null; + + Object@ obj; + file >> obj; + + if(obj is null) + return null; + + for(uint i = 0, cnt = factories.length; i < cnt; ++i) { + if(factories[i].obj is obj) + return factories[i]; + } + + Factory f; + @f.obj = obj; + factories.insertLast(f); + return f; + } + + array savedConstructions; + array loadedConstructions; + void postSave(AI& ai) { + savedConstructions.length = 0; + } + void postLoad(AI& ai) { + loadedConstructions.length = 0; + } + + void saveConstruction(SaveFile& file, AllocateConstruction@ alloc) { + if(alloc is null) { + file.write0(); + return; + } + + file.write1(); + file << alloc.id; + if(alloc.id == -1) { + storeConstruction(file, alloc); + } + else { + bool found = false; + for(uint i = 0, cnt = savedConstructions.length; i < cnt; ++i) { + if(savedConstructions[i] is alloc) { + found = true; + break; + } + } + + if(!found) { + storeConstruction(file, alloc); + savedConstructions.insertLast(alloc); + } + } + } + + AllocateConstruction@ loadConstruction(SaveFile& file) { + if(!file.readBit()) + return null; + + int id = 0; + file >> id; + if(id == -1) { + AllocateConstruction@ alloc = createConstruction(file); + alloc.id = id; + return alloc; + } + else { + for(uint i = 0, cnt = loadedConstructions.length; i < cnt; ++i) { + if(loadedConstructions[i].id == id) + return loadedConstructions[i]; + } + + AllocateConstruction@ alloc = createConstruction(file); + alloc.id = id; + loadedConstructions.insertLast(alloc); + return alloc; + } + } + + void storeConstruction(SaveFile& file, AllocateConstruction@ alloc) { + auto@ cls = getClass(alloc); + auto@ mod = cls.module; + + file << mod.name; + file << cls.name; + alloc._save(this, file); + } + + AllocateConstruction@ createConstruction(SaveFile& file) { + string modName; + string clsName; + + file >> modName; + file >> clsName; + + auto@ mod = getScriptModule(modName); + if(mod is null) { + error("ERROR: AI Load could not find module for alloc "+modName+"::"+clsName); + return null; + } + + auto@ cls = mod.getClass(clsName); + if(cls is null) { + error("ERROR: AI Load could not find class for alloc "+modName+"::"+clsName); + return null; + } + + auto@ alloc = cast(cls.create()); + if(alloc is null) { + error("ERROR: AI Load could not create class instance for alloc "+modName+"::"+clsName); + return null; + } + + alloc._load(this, file); + return alloc; + } + + void start() { + Object@ hw = ai.empire.Homeworld; + if(hw !is null) { + Factory f; + @f.obj = hw; + @f.plAI = planets.getAI(cast(hw)); + + factories.insertLast(f); + } + } + + Factory@ get(Object@ obj) { + for(uint i = 0, cnt = factories.length; i < cnt; ++i) { + if(factories[i].obj is obj) + return factories[i]; + } + return null; + } + + Factory@ registerFactory(Object@ obj) { + for(uint i = 0, cnt = factories.length; i < cnt; ++i) { + if(factories[i].obj is obj) + return factories[i]; + } + Factory f; + @f.obj = obj; + factories.insertLast(f); + return f; + } + + Factory@ getFactory(Region@ region) { + Factory@ best; + double bestLabor = 0; + for(uint i = 0, cnt = factories.length; i < cnt; ++i) { + if(factories[i].obj.region !is region) + continue; + double l = factories[i].obj.laborIncome; + if(l > bestLabor) { + bestLabor = l; + @best = factories[i]; + } + } + return best; + } + + BuildConstruction@ buildConstruction(const ConstructionType@ type, double priority = 1.0, bool force = false, uint moneyType = BT_Development) { + //Potentially build a flagship + BuildConstruction f(type); + f.moneyType = moneyType; + f.priority = priority; + build(f, force=force); + return f; + } + + BuildFlagship@ buildFlagship(const Design@ dsg, double priority = 1.0, bool force = false) { + //Potentially build a flagship + BuildFlagship f(dsg); + f.moneyType = BT_Military; + f.priority = priority; + build(f, force=force); + return f; + } + + BuildFlagship@ buildFlagship(DesignTarget@ target, double priority = 1.0, bool force = false) { + //Potentially build a flagship + BuildFlagship f(target); + f.moneyType = BT_Military; + f.priority = priority; + build(f, force=force); + return f; + } + + BuildStation@ buildStation(const Design@ dsg, const vec3d& position, double priority = 1.0, bool force = false) { + //Potentially build a flagship + BuildStation f(dsg, position); + f.moneyType = BT_Military; + f.priority = priority; + build(f, force=force); + return f; + } + + BuildStation@ buildStation(DesignTarget@ target, const vec3d& position, double priority = 1.0, bool force = false) { + //Potentially build a flagship + BuildStation f(target, position); + f.moneyType = BT_Military; + f.priority = priority; + build(f, force=force); + return f; + } + + BuildOrbital@ buildOrbital(const OrbitalModule@ module, const vec3d& position, double priority = 1.0, bool force = false, uint moneyType = BT_Infrastructure) { + //Potentially build a flagship + BuildOrbital f(module, position); + f.moneyType = moneyType; + f.priority = priority; + build(f, force=force); + return f; + } + + BuildStation@ buildLocalStation(const Design@ dsg, double priority = 1.0, bool force = false) { + //Potentially build a flagship + BuildStation f(dsg, local=true); + f.moneyType = BT_Military; + f.priority = priority; + build(f, force=force); + return f; + } + + BuildStation@ buildLocalStation(DesignTarget@ target, double priority = 1.0, bool force = false) { + //Potentially build a flagship + BuildStation f(target, local=true); + f.moneyType = BT_Military; + f.priority = priority; + build(f, force=force); + return f; + } + + BuildOrbital@ buildLocalOrbital(const OrbitalModule@ module, double priority = 1.0, bool force = false, uint moneyType = BT_Infrastructure) { + //Potentially build a flagship + BuildOrbital f(module, local=true); + f.moneyType = moneyType; + f.priority = priority; + build(f, force=force); + return f; + } + + BuildOrbital@ buildLocalOrbital(const OrbitalModule@ module, Planet@ planet, double priority = 1.0, bool force = false, uint moneyType = BT_Infrastructure) { + //Potentially build a flagship + BuildOrbital f(module, planet); + f.moneyType = moneyType; + f.priority = priority; + build(f, force=force); + return f; + } + + RetrofitShip@ retrofit(Ship@ ship, double priority = 1.0, bool force = false) { + //Potentially build a flagship + RetrofitShip f(ship); + f.moneyType = BT_Military; + f.priority = priority; + build(f, force=force); + return f; + } + + AllocateConstruction@ build(AllocateConstruction@ alloc, bool force = false) { + //Add a construction into the potential constructions queue + if(!force) + alloc.maxTime = ai.behavior.constructionMaxTime; + alloc.id = nextAllocId++; + allocations.insertLast(alloc); + + if(log) + ai.print("Queue construction: "+alloc.toString()); + + return alloc; + } + + AllocateConstruction@ buildNow(AllocateConstruction@ alloc, Factory@ f) { + if(f.busy) + return null; + if(alloc.alloc !is null) + budget.applyNow(alloc.alloc); + start(f, alloc); + allocations.remove(alloc); + return alloc; + } + + void cancel(AllocateConstruction@ alloc) { + if(alloc.started || (alloc.alloc !is null && alloc.alloc.allocated)) + return; //TODO + + allocations.remove(alloc); + for(uint i = 0, cnt = factories.length; i < cnt; ++i) { + if(factories[i].active is alloc) + @factories[i].active = null; + } + + if(alloc.alloc !is null) + budget.remove(alloc.alloc); + } + + uint factInd = 0; + void tick(double time) { + //Manage factories + if(factories.length != 0) { + factInd = (factInd+1) % factories.length; + auto@ f = factories[factInd]; + if(!f.tick(ai, this, time)) + factories.removeAt(factInd); + } + } + + void start(Factory@ f, AllocateConstruction@ c) { + if(ai.behavior.forbidConstruction) { + cancel(c); + return; + } + //Actually construct something we've allocated budget for + @f.active = c; + @c.tryFactory = null; + + c.construct(ai, f); + + if(log) + ai.print("Construct: "+c.toString(), f.obj); + + for(uint i = 0, cnt = allocations.length; i < cnt; ++i) { + if(allocations[i].tryFactory is f) + @allocations[i].tryFactory = null; + } + } + + uint plCheck = 0; + uint orbCheck = 0; + double consTimer = 0.0; + void focusTick(double time) { + //Progress the allocations + for(uint n = 0, ncnt = allocations.length; n < ncnt; ++n) { + if(!allocations[n].tick(ai, this, time)) { + allocations.removeAt(n); + --n; --ncnt; + } + } + + if(ai.behavior.forbidConstruction) return; + + //See if anything we can potentially construct is constructible + totalLabor = 0.0; + bestLabor = 0.0; + for(uint i = 0, cnt = factories.length; i < cnt; ++i) { + auto@ f = factories[i]; + totalLabor += f.laborIncome; + if(f.laborIncome > bestLabor) + bestLabor = f.laborIncome; + } + + for(uint n = 0, ncnt = allocations.length; n < ncnt; ++n) { + auto@ alloc = allocations[n]; + if(alloc.tryFactory !is null) + continue; + + Factory@ bestFact; + double bestCur = 0.0; + + for(uint i = 0, cnt = factories.length; i < cnt; ++i) { + auto@ f = factories[i]; + if(f.busy) + continue; + if(!alloc.canBuild(ai, f)) + continue; + if(!f.viable(ai, alloc)) + continue; + + double w = f.laborIncome; + if(f is primaryFactory) + w *= 1.5; + if(f.exportingTo !is null) + w /= 0.75; + + if(w > bestCur) { + bestCur = w; + @bestFact = f; + } + } + + if(bestFact !is null) { + @alloc.tryFactory = bestFact; + alloc.update(ai, bestFact); + } + } + + //Classify our primary factory + if(primaryFactory is null) { + //Find our best factory + Factory@ best; + double bestWeight = 0.0; + for(uint i = 0, cnt = factories.length; i < cnt; ++i) { + auto@ f = factories[i]; + + double w = f.laborIncome; + w += 0.1 * f.laborAim; + if(f.obj.isPlanet) + w *= 100.0; + if(f.obj.isShip) + w *= 0.1; + + if(w > bestWeight) { + bestWeight = w; + @best = f; + } + } + + if(best !is null) { + @primaryFactory = best; + } + else { + noFactoryTimer += time; + if(noFactoryTimer > 3.0 * 60.0 && ai.defs.Factory !is null) { + //Just pick our highest level planet and hope for the best + PlanetAI@ best; + double bestWeight = 0.0; + + for(uint i = 0, cnt = planets.planets.length; i < cnt; ++i) { + auto@ plAI = planets.planets[i]; + double w = plAI.obj.level; + w += 0.5 * plAI.obj.resourceLevel; + + if(w > bestWeight) { + bestWeight = w; + @best = plAI; + } + } + + if(best !is null) { + Factory f; + @f.obj = best.obj; + @f.plAI = best; + + factories.insertLast(f); + @primaryFactory = f; + } + + noFactoryTimer = 0.0; + } + } + } + else { + noFactoryTimer = 0.0; + } + + //Find new factories + if(planets.planets.length != 0) { + plCheck = (plCheck+1) % planets.planets.length; + PlanetAI@ plAI = planets.planets[plCheck]; + if(plAI.obj.laborIncome > 0 && plAI.obj.canBuildShips) { + if(get(plAI.obj) is null) { + Factory f; + @f.obj = plAI.obj; + @f.plAI = plAI; + + factories.insertLast(f); + } + } + } + if(orbitals.orbitals.length != 0) { + orbCheck = (orbCheck+1) % orbitals.orbitals.length; + OrbitalAI@ orbAI = orbitals.orbitals[orbCheck]; + if(orbAI.obj.hasConstruction && orbAI.obj.laborIncome > 0 + && !cast(orbAI.obj).hasMaster()) { + if(get(orbAI.obj) is null) { + Factory f; + @f.obj = orbAI.obj; + + factories.insertLast(f); + } + } + } + + //See if we should switch our primary factory + if(primaryFactory !is null) { + if(!primaryFactory.valid) { + @primaryFactory = null; + } + else { + Factory@ best; + double bestLabor = 0.0; + double primaryLabor = primaryFactory.laborIncome; + bool canImport = primaryFactory.obj.canImportLabor; + for(uint i = 0, cnt = factories.length; i < cnt; ++i) { + auto@ f = factories[i]; + double checkLabor = f.laborIncome; + if(f.obj.isShip) + checkLabor *= 0.1; + if(f.exportingTo !is primaryFactory && canImport) + primaryLabor += checkLabor * 0.75; + if(checkLabor > bestLabor) { + bestLabor = checkLabor; + @best = f; + } + } + + if(best !is null && bestLabor > 1.5 * primaryLabor) + @primaryFactory = best; + } + } + + //See if we should consolidate at a shipyard + if(buildConsolidate !is null && buildConsolidate.completed) { + @buildConsolidate = null; + consTimer = gameTime + 60.0; + } + else if(ai.behavior.consolidateLaborExports && primaryFactory !is null && ai.defs.Shipyard !is null && buildConsolidate is null && !primaryFactory.obj.canImportLabor && consTimer < gameTime) { + double totalLabor = 0.0, bestLabor = 0.0; + for(uint i = 0, cnt = factories.length; i < cnt; ++i) { + double inc = factories[i].obj.baseLaborIncome; + if(factories[i].obj.canExportLabor) + totalLabor += inc; + if(factories[i].laborIncome > bestLabor) + bestLabor = factories[i].laborIncome; + } + + if(bestLabor < totalLabor * 0.6) { + Factory@ bestConsolidate; + double bestWeight = 0.0; + for(uint i = 0, cnt = factories.length; i < cnt; ++i) { + auto@ f = factories[i]; + if(!f.obj.canImportLabor) + continue; + + double w = f.obj.baseLaborIncome; + w /= f.obj.position.distanceTo(primaryFactory.obj.position); + + if(w > bestWeight) { + bestWeight = w; + @bestConsolidate = f; + } + } + + if(bestConsolidate !is null) { + if(log) + ai.print("Set shipyard for consolidate.", bestConsolidate.obj.region); + @primaryFactory = bestConsolidate; + } + else { + Region@ reg = primaryFactory.obj.region; + if(reg !is null) { + vec3d pos = reg.position; + vec2d offset = random2d(reg.radius * 0.4, reg.radius * 0.8); + pos.x += offset.x; + pos.z += offset.y; + + if(log) + ai.print("Build shipyard for consolidate.", reg); + + @buildConsolidate = buildOrbital(ai.defs.Shipyard, pos); + } + } + } + } + } + + bool isGettingAsteroid(Asteroid@ asteroid) { + for(uint i = 0, cnt = factories.length; i < cnt; ++i) { + if(factories[i].bgAsteroid is asteroid) + return true; + } + return false; + } + + Asteroid@ getBackgroundAsteroid(Factory& f) { + double closest = INFINITY; + Asteroid@ best; + Region@ reg = f.obj.region; + if(reg is null) + return null; + + uint cnt = systems.owned.length; + uint offset = randomi(0, cnt-1); + for(uint i = 0, check = min(3, cnt); i < check; ++i) { + auto@ sys = systems.owned[(i+offset)%cnt]; + double dist = sys.obj.position.distanceToSQ(f.obj.position); + if(dist > closest) + continue; + if(!sys.obj.sharesTerritory(ai.empire, reg)) + continue; + + for(uint n = 0, ncnt = sys.asteroids.length; n < ncnt; ++n) { + Asteroid@ roid = sys.asteroids[n]; + if(roid.owner.valid) + continue; + if(roid.getAvailableCount() == 0) + continue; + if(isGettingAsteroid(roid)) + continue; + + closest = dist; + @best = roid; + break; + } + } + + cnt = systems.outsideBorder.length; + offset = randomi(0, cnt-1); + for(uint i = 0, check = min(3, cnt); i < check; ++i) { + auto@ sys = systems.outsideBorder[(i+offset)%cnt]; + double dist = sys.obj.position.distanceToSQ(f.obj.position); + if(dist > closest) + continue; + if(!sys.obj.sharesTerritory(ai.empire, reg)) + continue; + + for(uint n = 0, ncnt = sys.asteroids.length; n < ncnt; ++n) { + Asteroid@ roid = sys.asteroids[n]; + if(roid.owner.valid) + continue; + if(roid.getAvailableCount() == 0) + continue; + if(isGettingAsteroid(roid)) + continue; + + closest = dist; + @best = roid; + break; + } + } + + return best; + } +}; + +double asteroidResourceValue(const ResourceType@ type) { + if(type is null) + return 0.0; + double w = 1.0; + w += type.level * 10.0; + w += type.totalPressure; + if(type.cls !is null) + w += 5.0; + return w; +} + +AIComponent@ createConstruction() { + return Construction(); +} ADDED scripts/server/empire_ai/weasel/Creeping.as Index: scripts/server/empire_ai/weasel/Creeping.as ================================================================== --- scripts/server/empire_ai/weasel/Creeping.as +++ scripts/server/empire_ai/weasel/Creeping.as @@ -0,0 +1,619 @@ +// Creeping +// -------- +// Uses fleets that aren't currently doing anything to eliminate creeps. +// + +import empire_ai.weasel.WeaselAI; + +import empire_ai.weasel.Fleets; +import empire_ai.weasel.Systems; +import empire_ai.weasel.Movement; +import empire_ai.weasel.searches; + +import saving; +from empire import Creeps; + +class CreepingMission : Mission { + Pickup@ pickup; + Object@ protector; + + MoveOrder@ move; + + void save(Fleets& fleets, SaveFile& file) { + file << pickup; + file << protector; + fleets.movement.saveMoveOrder(file, move); + } + + void load(Fleets& fleets, SaveFile& file) { + file >> pickup; + file >> protector; + @move = fleets.movement.loadMoveOrder(file); + } + + void start(AI& ai, FleetAI& fleet) override { + vec3d position = pickup.position; + double dist = fleet.radius; + if(protector !is null && protector.valid) + dist = fleet.obj.getEngagementRange(); + position += (fleet.obj.position - pickup.position).normalized(dist); + + @move = cast(ai.movement).move(fleet.obj, position); + } + + void tick(AI& ai, FleetAI& fleet, double time) { + if(move !is null) { + if(move.completed) { + if(protector !is null && protector.valid) { + if(!protector.isVisibleTo(ai.empire)) { //Yo nebulas are scary yo + fleet.obj.addMoveOrder(protector.position); + fleet.obj.addAttackOrder(protector, append=true); + } + else { + fleet.obj.addAttackOrder(protector); + } + } + @move = null; + } + else if(move.failed) { + canceled = true; + return; + } + else + return; + } + if(protector is null || !protector.valid) { + if(!fleet.obj.hasOrders) { + if(pickup is null || !pickup.valid) { + if(cast(ai.creeping).log) + ai.print("Finished clearing creep camp", fleet.obj); + completed = true; + } + else { + fleet.obj.addPickupOrder(pickup); + @protector = null; + } + } + } + else { + if((fleet.filled < 0.3 || fleet.supplies < 0.3 || fleet.flagshipHealth < 0.4) + && protector.getFleetStrength() * ai.behavior.remnantOverkillFactor > fleet.strength) { + //Holy shit what's going on? ABORT! ABORT! + if(cast(ai.creeping).logCritical) + ai.print("ABORTED CREEPING: About to lose fight", fleet.obj); + canceled = true; + cast(ai.fleets).returnToBase(fleet, MP_Critical); + } + } + } +}; + +class ClearMission : Mission { + Region@ region; + Object@ eliminate; + + MoveOrder@ move; + + void save(Fleets& fleets, SaveFile& file) { + file << region; + file << eliminate; + fleets.movement.saveMoveOrder(file, move); + } + + void load(Fleets& fleets, SaveFile& file) { + file >> region; + file >> eliminate; + @move = fleets.movement.loadMoveOrder(file); + } + + void start(AI& ai, FleetAI& fleet) override { + @move = cast(ai.movement).move(fleet.obj, region); + } + + void tick(AI& ai, FleetAI& fleet, double time) { + if(move !is null) { + if(move.completed) { + @move = null; + } + else if(move.failed) { + canceled = true; + return; + } + else + return; + } + + if(ai.behavior.forbidCreeping) return; + + if(eliminate is null) { + @eliminate = cast(ai.creeping).findRemnants(region); + if(eliminate is null) { + completed = true; + return; + } + } + + if(eliminate !is null) { + if(!eliminate.valid) { + @eliminate = null; + } + else { + if(!fleet.obj.hasOrders) + fleet.obj.addAttackOrder(eliminate); + + if((fleet.filled < 0.3 || fleet.supplies < 0.3 || fleet.flagshipHealth < 0.4) + && eliminate.getFleetStrength() * ai.behavior.remnantOverkillFactor > fleet.strength) { + //Holy shit what's going on? ABORT! ABORT! + if(cast(ai.creeping).logCritical) + ai.print("ABORTED CREEPING: About to lose fight", fleet.obj); + canceled = true; + cast(ai.fleets).returnToBase(fleet, MP_Critical); + } + } + } + } +}; + +final class CreepPenalty : Savable { + Object@ obj; + double until; + + void save(SaveFile& file) { + file << obj; + file << until; + } + + void load(SaveFile& file) { + file >> obj; + file >> until; + } +}; + +final class ClearSystem { + SystemAI@ sys; + array remnants; + + void save(Creeping& creeping, SaveFile& file) { + creeping.systems.saveAI(file, sys); + uint cnt = remnants.length; + file << cnt; + for(uint i = 0; i < cnt; ++i) + file << remnants[i]; + } + + void load(Creeping& creeping, SaveFile& file) { + @sys = creeping.systems.loadAI(file); + uint cnt = 0; + file >> cnt; + for(uint i = 0; i < cnt; ++i) { + Ship@ remn; + file >> remn; + if(remn !is null) + remnants.insertLast(remn); + } + } + + void record() { + auto@ objs = findEnemies(sys.obj, null, Creeps.mask); + for(uint i = 0, cnt = objs.length; i < cnt; ++i) { + Ship@ ship = cast(objs[i]); + if(ship !is null) + remnants.insertLast(ship); + } + } + + double getStrength() { + double str = 0.0; + for(uint i = 0, cnt = remnants.length; i < cnt; ++i) { + if(remnants[i].valid) + str += sqrt(remnants[i].getFleetStrength()); + } + return str * str; + } +}; + +class Creeping : AIComponent { + Systems@ systems; + Fleets@ fleets; + + array requested; + array penalties; + array active; + + array quarantined; + + void create() { + @systems = cast(ai.systems); + @fleets = cast(ai.fleets); + } + + void save(SaveFile& file) { + uint cnt = requested.length; + file << cnt; + for(uint i = 0; i < cnt; ++i) + systems.saveAI(file, requested[i]); + + cnt = penalties.length; + file << cnt; + for(uint i = 0; i < cnt; ++i) + file << penalties[i]; + + cnt = active.length; + file << cnt; + for(uint i = 0; i < cnt; ++i) + fleets.saveMission(file, active[i]); + + cnt = quarantined.length; + file << cnt; + for(uint i = 0; i < cnt; ++i) + quarantined[i].save(this, file); + } + + void load(SaveFile& file) { + uint cnt = 0; + file >> cnt; + for(uint i = 0; i < cnt; ++i) { + auto@ sys = systems.loadAI(file); + if(sys !is null) + requested.insertLast(sys); + } + + file >> cnt; + for(uint i = 0; i < cnt; ++i) { + CreepPenalty pen; + file >> pen; + if(pen.obj !is null) + penalties.insertLast(pen); + } + + file >> cnt; + for(uint i = 0; i < cnt; ++i) { + auto@ miss = cast(fleets.loadMission(file)); + if(miss !is null) + active.insertLast(miss); + } + + if(file >= SV_0151) { + file >> cnt; + for(uint i = 0; i < cnt; ++i) { + ClearSystem qsys; + qsys.load(this, file); + quarantined.insertLast(qsys); + } + } + } + + void requestClear(SystemAI@ system) { + if(system is null) + return; + if(log) + ai.print("Requested creep camp clear", system.obj); + if(requested.find(system) == -1) + requested.insertLast(system); + } + + CreepingMission@ creepWithFleet(FleetAI@ fleet, Pickup@ pickup, Object@ protector = null) { + if(protector is null) + @protector = pickup.getProtector(); + + if(log) + ai.print("Clearing creep camp in "+pickup.region.name, fleet.obj); + + CreepingMission mission; + @mission.pickup = pickup; + @mission.protector = protector; + + fleets.performMission(fleet, mission); + active.insertLast(mission); + return mission; + } + + Pickup@ best; + Object@ bestProtector; + vec3d ourPosition; + double bestWeight; + double ourStrength; + + void check(SystemAI@ sys, double weight = 1.0) { + for(uint n = 0, ncnt = sys.pickups.length; n < ncnt; ++n) { + Pickup@ pickup = sys.pickups[n]; + Object@ protector = sys.pickupProtectors[n]; + + if(!pickup.valid) + continue; + + double protStrength; + if(protector !is null && protector.valid) { + protStrength = protector.getFleetStrength(); + + if(protStrength * ai.behavior.remnantOverkillFactor > ourStrength) + continue; + } + else + protStrength = 1.0; + + if(isCreeping(pickup)) + continue; + + double w = weight; + w /= protStrength / 1000.0; + w /= pickup.position.distanceTo(ourPosition); + + if(w > bestWeight) { + bestWeight = w; + @best = pickup; + @bestProtector = protector; + } + } + } + + void penalize(Object@ obj, double time) { + for(uint i = 0, cnt = penalties.length; i < cnt; ++i) { + if(penalties[i].obj is obj) { + penalties[i].until = max(penalties[i].until, gameTime + time); + return; + } + } + + CreepPenalty p; + @p.obj = obj; + p.until = gameTime + time; + penalties.insertLast(p); + } + + bool isPenalized(Object@ obj) { + for(uint i = 0, cnt = penalties.length; i < cnt; ++i) { + if(penalties[i].obj is obj) + return true; + } + return false; + } + + bool isCreeping(Pickup@ pickup) { + for(uint i = 0, cnt = active.length; i < cnt; ++i) { + if(active[i].pickup is pickup) + return true; + } + return false; + } + + CreepingMission@ creepWithFleet(FleetAI@ fleet) { + @best = null; + @bestProtector = null; + bestWeight = 0.0; + ourStrength = fleet.strength; + ourPosition = fleet.obj.position; + + //Check requested systems first + for(uint i = 0, cnt = requested.length; i < cnt; ++i) { + auto@ sys = requested[i]; + + if(sys.pickups.length == 0) { + requested.removeAt(i); + --i; --cnt; + continue; + } + + if(haveQuarantinedSystem(sys)) + continue; + + check(sys); + } + + if(best !is null) + return creepWithFleet(fleet, best, bestProtector); + + if(!ai.behavior.remnantAllowArbitraryClear) + return null; + + if(log) + ai.print("Attempted to find creep camp to clear", fleet.obj); + + //Check systems in our territory + for(uint i = 0, cnt = systems.owned.length; i < cnt; ++i) { + SystemAI@ sys = systems.owned[i]; + if(sys.pickups.length != 0) + check(sys); + } + + if(best !is null) + return creepWithFleet(fleet, best, bestProtector); + + //Check systems just outside our border + for(uint i = 0, cnt = systems.outsideBorder.length; i < cnt; ++i) { + SystemAI@ sys = systems.outsideBorder[i]; + if(sys.seenPresent & ai.otherMask != 0) + continue; + if(haveQuarantinedSystem(sys)) + continue; + if(sys.pickups.length != 0) + check(sys, 1.0 / double(1.0 + sys.hopDistance)); + } + + if(best !is null) + return creepWithFleet(fleet, best, bestProtector); + + penalize(fleet.obj, 90.0); + return null; + } + + Object@ findRemnants(Region@ reg) { + for(uint i = 0, cnt = quarantined.length; i < cnt; ++i) { + auto@ qsys = quarantined[i]; + if(qsys.sys.obj !is reg) + continue; + + for(uint n = 0, ncnt = qsys.remnants.length; n < ncnt; ++n) { + auto@ remn = qsys.remnants[n]; + if(remn is null || !remn.valid) + continue; + return remn; + } + } + return null; + } + + ClearMission@ sendToClear(FleetAI@ fleet, ClearSystem@ system) { + ClearMission miss; + @miss.region = system.sys.obj; + + fleets.performMission(fleet, miss); + if(log) + ai.print("Clear remnant defenders in "+miss.region.name, fleet.obj); + return miss; + } + + bool isQuarantined(SystemAI@ sys) { + if(sys.planets.length == 0) + return false; + for(uint i = 0, cnt = sys.planets.length; i < cnt; ++i) { + if(!sys.planets[i].quarantined) + return false; + } + return true; + } + + bool isQuarantined(Region@ region) { + for(uint i = 0, cnt = quarantined.length; i < cnt; ++i) { + if(quarantined[i].sys.obj is region) + return true; + } + return false; + } + + bool haveQuarantinedSystem(SystemAI@ sys) { + for(uint i = 0, cnt = quarantined.length; i < cnt; ++i) { + if(quarantined[i].sys is sys) + return true; + } + return false; + } + + void recordQuarantinedSystem(SystemAI@ sys) { + ClearSystem qsys; + @qsys.sys = sys; + quarantined.insertLast(qsys); + + qsys.record(); + } + + uint ownedCheck = 0; + uint outsideCheck = 0; + void focusTick(double time) { + //Manage creeping check penalties + for(uint i = 0, cnt = penalties.length; i < cnt; ++i) { + if(penalties[i].until < gameTime) { + penalties.removeAt(i); + --i; --cnt; + } + } + + //Manage current creeping missions + for(uint i = 0, cnt = active.length; i < cnt; ++i) { + if(active[i].completed || active[i].canceled) { + active.removeAt(i); + --i; --cnt; + } + } + + //Find new systems that are quarantined + if(systems.owned.length != 0) { + ownedCheck = (ownedCheck+1) % systems.owned.length; + auto@ sys = systems.owned[ownedCheck]; + if(sys.explored && isQuarantined(sys)) { + if(!haveQuarantinedSystem(sys)) + recordQuarantinedSystem(sys); + } + } + if(systems.outsideBorder.length != 0) { + outsideCheck = (outsideCheck+1) % systems.outsideBorder.length; + auto@ sys = systems.outsideBorder[outsideCheck]; + if(sys.explored && isQuarantined(sys)) { + if(!haveQuarantinedSystem(sys)) + recordQuarantinedSystem(sys); + } + } + + //Update existing quarantined systems list + for(uint i = 0, cnt = quarantined.length; i < cnt; ++i) { + auto@ qsys = quarantined[i]; + if(!isQuarantined(qsys.sys)) { + quarantined.removeAt(i); + --i; --cnt; + continue; + } + for(uint n = 0, ncnt = qsys.remnants.length; n < ncnt; ++n) { + auto@ remn = qsys.remnants[n]; + if(remn is null || !remn.valid || remn.region !is qsys.sys.obj) { + qsys.remnants.removeAt(n); + --n; --ncnt; + } + } + } + + //See if we should try to clear a quarantined system + bool waitingForGather = false; + if(ai.behavior.remnantAllowArbitraryClear) { + ClearSystem@ best; + double bestStr = INFINITY; + + for(uint i = 0, cnt = quarantined.length; i < cnt; ++i) { + double str = quarantined[i].getStrength(); + if(quarantined[i].remnants.length == 0) + continue; + if(str < bestStr) { + bestStr = str; + @best = quarantined[i]; + } + } + + if(best !is null) { + double needStr = bestStr * ai.behavior.remnantOverkillFactor; + if(fleets.getTotalStrength(FC_Combat) > needStr) { + waitingForGather = true; + if(fleets.getTotalStrength(FC_Combat, readyOnly=true) > needStr) { + //Order sufficient fleets to go clear this system + double takeStr = sqrt(needStr); + double haveStr = 0.0; + + uint offset = randomi(0, fleets.fleets.length-1); + for(uint i = 0, cnt = fleets.fleets.length; i < cnt; ++i) { + FleetAI@ fleet = fleets.fleets[(i+offset)%cnt]; + if(fleet.fleetClass != FC_Combat) + continue; + if(!fleet.readyForAction) + continue; + + haveStr += sqrt(fleet.strength); + sendToClear(fleet, best); + + if(haveStr > takeStr) + break; + } + } + } + } + } + + //Find new fleets to creep with + if(!waitingForGather) { + uint offset = randomi(0, fleets.fleets.length-1); + for(uint i = 0, cnt = fleets.fleets.length; i < cnt; ++i) { + FleetAI@ fleet = fleets.fleets[(i+offset)%cnt]; + if(fleet.fleetClass != FC_Combat) + continue; + if(!fleet.readyForAction) + continue; + if(isPenalized(fleet.obj)) + continue; + + creepWithFleet(fleet); + break; + } + } + } +}; + +AIComponent@ createCreeping() { + return Creeping(); +} ADDED scripts/server/empire_ai/weasel/Designs.as Index: scripts/server/empire_ai/weasel/Designs.as ================================================================== --- scripts/server/empire_ai/weasel/Designs.as +++ scripts/server/empire_ai/weasel/Designs.as @@ -0,0 +1,690 @@ +import empire_ai.weasel.WeaselAI; + +import util.design_export; +import util.random_designs; + +interface RaceDesigns { + bool preCompose(DesignTarget@ target); + bool postCompose(DesignTarget@ target); + bool design(DesignTarget@ target, int size, const Design@& output); +}; + +enum DesignPurpose { + DP_Scout, + DP_Combat, + DP_Defense, + DP_Support, + DP_Gate, + DP_Slipstream, + DP_Mothership, + DP_Miner, + + DP_COUNT, + DP_Unknown, +}; + +tidy final class DesignTarget { + int id = -1; + const Design@ active; + string customName; + + array potential; + array scores; + + uint purpose; + double targetBuildCost = 0; + double targetMaintenance = 0; + double targetLaborCost = 0; + + double dps = 0.0; + double hp = 0.0; + double supplyDrain = 0.0; + + double targetSize = 0; + bool findSize = false; + + Designer@ designer; + + DesignTarget() { + } + + DesignTarget(uint type, double targetSize) { + this.purpose = type; + this.targetSize = targetSize; + } + + uint get_designType() { + switch(purpose) { + case DP_Scout: return DT_Flagship; + case DP_Combat: return DT_Flagship; + case DP_Defense: return DT_Station; + case DP_Support: return DT_Support; + case DP_Gate: return DT_Station; + case DP_Slipstream: return DT_Flagship; + case DP_Mothership: return DT_Flagship; + } + return DT_Flagship; + } + + void save(Designs& designs, SaveFile& file) { + if(active !is null) { + file.write1(); + file << active; + file << dps; + file << supplyDrain; + file << hp; + } + else { + file.write0(); + } + + file << purpose; + file << targetBuildCost; + file << targetMaintenance; + file << targetLaborCost; + file << targetSize; + file << findSize; + file << customName; + } + + void load(Designs& designs, SaveFile& file) { + if(file.readBit()) { + file >> active; + file >> dps; + file >> supplyDrain; + file >> hp; + } + + file >> purpose; + file >> targetBuildCost; + file >> targetMaintenance; + file >> targetLaborCost; + file >> targetSize; + file >> findSize; + file >> customName; + } + + void prepare(AI& ai) { + @designer = Designer(designType, targetSize, ai.empire, compose=false); + designer.randomHull = true; + + switch(purpose) { + case DP_Scout: + designer.composeScout(); + break; + case DP_Combat: + designer.composeFlagship(); + break; + case DP_Defense: + designer.composeStation(); + break; + case DP_Support: + designer.composeSupport(); + break; + case DP_Gate: + designer.composeGate(); + break; + case DP_Slipstream: + designer.composeSlipstream(); + break; + case DP_Mothership: + designer.composeMothership(); + break; + } + } + + double weight(double value, double goal) { + if(value < goal) + return sqr(value / goal); + else if(value > goal * 1.5) + return goal / value; + return 1.0; + } + + double costWeight(double value, double goal) { + if(findSize) { + if(value < goal) + return 1.0; + else + return 0.000001; + } + else { + if(value < goal) + return goal / value; + else + return pow(0.2, ((value / goal) - 1.0) * 10.0); + } + } + + double evaluate(AI& ai, const Design& dsg) { + double w = 1.0; + + //Try to stick as close to our target as we can + if(targetBuildCost != 0) + w *= costWeight(dsg.total(HV_BuildCost), targetBuildCost); + if(targetLaborCost != 0) + w *= costWeight(dsg.total(HV_LaborCost), targetLaborCost); + if(targetMaintenance != 0) + w *= costWeight(dsg.total(HV_MaintainCost), targetMaintenance); + + double predictHP = 0.0; + double predictDPS = 0.0; + double predictDrain = 0.0; + + //Value support capacity where appropriate + if(purpose == DP_Combat) { + double supCap = dsg.total(SV_SupportCapacity); + double avgHP = 0, avgDPS = 0, avgDrain = 0.0; + cast(ai.designs).getSupportAverages(avgHP, avgDPS, avgDrain); + + predictHP += supCap * avgHP; + predictDPS += supCap * avgDPS; + predictDrain += supCap * avgDrain; + } + + //Value combat strength where appropriate + if(purpose != DP_Scout && purpose != DP_Slipstream && purpose != DP_Mothership) { + predictDPS += dsg.total(SV_DPS); + predictHP += dsg.totalHP + dsg.total(SV_ShieldCapacity); + predictDrain += dsg.total(SV_SupplyDrain); + + if(purpose != DP_Support) { + w *= (predictHP * predictDPS) * 0.001; + + double supplyStores = dsg.total(SV_SupplyCapacity); + double actionTime = supplyStores / predictDrain; + w *= weight(actionTime, ai.behavior.fleetAimSupplyDuration); + } + } + + //Value acceleration on a target + if(purpose != DP_Defense && purpose != DP_Gate) { + double targetAccel = 2.0; + if(purpose == DP_Support) + targetAccel *= 1.5; + else if(purpose == DP_Scout) + targetAccel *= 3.0; + + w *= weight(dsg.total(SV_Thrust) / max(dsg.total(HV_Mass), 0.01), targetAccel); + } + + //Penalties for having important systems easy to shoot down + uint holes = 0; + for(uint i = 0, cnt = dsg.subsystemCount; i < cnt; ++i) { + auto@ sys = dsg.subsystem(i); + if(!sys.type.hasTag(ST_Important)) + continue; + //TODO: We should be able to penalize for exposed supply storage + if(sys.type.hasTag(ST_NoCore)) + continue; + + vec2u core = sys.core; + for(uint d = 0; d < 6; ++d) { + if(!traceContainsArmor(dsg, core, d)) + holes += 1; + } + } + + if(holes != 0) + w /= pow(0.9, double(holes)); + + //TODO: Check FTL + + return w; + } + + bool traceContainsArmor(const Design@ dsg, const vec2u& startPos, uint direction) { + vec2u pos = startPos; + while(dsg.hull.active.valid(pos)) { + if(!dsg.hull.active.advance(pos, HexGridAdjacency(direction))) + break; + + auto@ sys = dsg.subsystem(pos.x, pos.y); + if(sys is null) + continue; + if(sys.type.hasTag(ST_IsArmor)) + return true; + } + return false; + } + + bool contains(const Design& dsg) { + if(active is null) + return false; + if(dsg.mostUpdated() is active.mostUpdated()) + return true; + return false; + } + + const Design@ design(AI& ai, Designs& designs) { + int trySize = targetSize; + if(findSize) { + trySize = randomd(0.75, 1.25) * targetSize; + trySize = 5 * round(double(designer.size) / 5.0); + } + if(designs.race !is null) { + const Design@ fromRace; + if(designs.race.design(this, trySize, fromRace)) + return fromRace; + } + if(designer !is null) { + designer.size = trySize; + return designer.design(1); + } + return null; + } + + void choose(AI& ai, const Design@ dsg, bool randomizeName=true) { + set(dsg); + @designer = null; + findSize = false; + + string baseName = dsg.name; + if(customName.length != 0) { + baseName = customName; + } + else if(randomizeName) { + if(dsg.hasTag(ST_IsSupport)) + baseName = autoSupportNames[randomi(0,autoSupportNames.length-1)]; + else + baseName = autoFlagNames[randomi(0,autoFlagNames.length-1)]; + } + + string name = baseName; + uint try = 0; + while(ai.empire.getDesign(name) !is null) { + name = baseName + " "; + appendRoman(++try, name); + } + if(name != dsg.name) + dsg.rename(name); + + //Set design settings/support behavior + if(purpose == DP_Support) { + if(dsg.total(SV_SupportSupplyCapacity) > 0.01) { + DesignSettings settings; + settings.behavior = SG_Brawler; + dsg.setSettings(settings); + } + else if(dsg.totalHP > 50 * dsg.size) { + DesignSettings settings; + settings.behavior = SG_Shield; + dsg.setSettings(settings); + } + else { + DesignSettings settings; + settings.behavior = SG_Cannon; + dsg.setSettings(settings); + } + } + + + ai.empire.addDesign(ai.empire.getDesignClass("AI", true), dsg); + + if(cast(ai.designs).log) + ai.print("Chose design for purpose "+uint(purpose)+" at size "+dsg.size); + } + + void step(AI& ai, Designs& designs) { + if(active is null) { + if(designer is null) { + if(designs.race is null || !designs.race.preCompose(this)) + prepare(ai); + if(designs.race !is null && designs.race.postCompose(this)) + return; + } + if(potential.length >= ai.behavior.designEvaluateCount) { + //Find the best design out of all our potentials + const Design@ best; + double bestScore = 0.0; + + for(uint i = 0, cnt = potential.length; i < cnt; ++i) { + double w = scores[i]; + if(w > bestScore) { + bestScore = w; + @best = potential[i]; + } + } + potential.length = 0; + scores.length = 0; + + if(best !is null) + choose(ai, best); + } + else if(designer !is null && active is null) { + //Add a new design onto the list to be evaluated + const Design@ dsg = design(ai, designs); + if(dsg !is null && !dsg.hasFatalErrors()) { + potential.insertLast(dsg); + scores.insertLast(evaluate(ai, dsg)); + + /*if(designs.log)*/ + /* ai.print("Designed for purpose "+uint(purpose)+" at size "+dsg.size+", weight "+evaluate(ai, dsg));*/ + } + } + } + else { + set(active.mostUpdated()); + } + } + + void set(const Design@ dsg) { + if(active is dsg) + return; + + @active = dsg; + targetBuildCost = dsg.total(HV_BuildCost); + targetMaintenance = dsg.total(HV_MaintainCost); + targetLaborCost = dsg.total(HV_LaborCost); + targetSize = dsg.size; + + dps = dsg.total(SV_DPS); + hp = dsg.totalHP + dsg.total(SV_ShieldCapacity); + supplyDrain = dsg.total(SV_SupplyDrain); + } +}; + +const Design@ scaleDesign(const Design@ orig, int newSize) { + DesignDescriptor desc; + resizeDesign(orig, newSize, desc); + + return makeDesign(desc); +} + +final class Designs : AIComponent { + RaceDesigns@ race; + + int nextTargetId = 0; + array designing; + array completed; + array automatic; + + void create() { + @race = cast(ai.race); + } + + void start() { + //Design some basic support sizes + design(DP_Support, 1); + design(DP_Support, 2); + design(DP_Support, 4); + design(DP_Support, 8); + design(DP_Support, 16); + } + + void save(SaveFile& file) { + file << nextTargetId; + + uint cnt = designing.length; + file << cnt; + for(uint i = 0; i < cnt; ++i) { + saveDesign(file, designing[i]); + designing[i].save(this, file); + } + + cnt = automatic.length; + file << cnt; + for(uint i = 0; i < cnt; ++i) { + saveDesign(file, automatic[i]); + if(!isDesigning(automatic[i])) { + file.write1(); + automatic[i].save(this, file); + } + else { + file.write0(); + } + } + + cnt = completed.length; + file << cnt; + for(uint i = 0; i < cnt; ++i) { + saveDesign(file, completed[i]); + if(!isDesigning(completed[i])) { + file.write1(); + completed[i].save(this, file); + } + else { + file.write0(); + } + } + } + + bool isDesigning(DesignTarget@ targ) { + for(uint i = 0, cnt = designing.length; i < cnt; ++i) { + if(designing[i] is targ) + return true; + } + return false; + } + + void load(SaveFile& file) { + file >> nextTargetId; + + uint cnt = 0; + file >> cnt; + for(uint i = 0; i < cnt; ++i) { + auto@ targ = loadDesign(file); + targ.load(this, file); + designing.insertLast(targ); + } + + file >> cnt; + for(uint i = 0; i < cnt; ++i) { + auto@ targ = loadDesign(file); + if(file.readBit()) + targ.load(this, file); + automatic.insertLast(targ); + } + + file >> cnt; + for(uint i = 0; i < cnt; ++i) { + auto@ targ = loadDesign(file); + if(file.readBit()) + targ.load(this, file); + completed.insertLast(targ); + } + } + + array loadIds; + DesignTarget@ loadDesign(int id) { + if(id == -1) + return null; + for(uint i = 0, cnt = loadIds.length; i < cnt; ++i) { + if(loadIds[i].id == id) + return loadIds[i]; + } + DesignTarget data; + data.id = id; + loadIds.insertLast(data); + return data; + } + DesignTarget@ loadDesign(SaveFile& file) { + int id = -1; + file >> id; + if(id == -1) + return null; + else + return loadDesign(id); + } + void saveDesign(SaveFile& file, DesignTarget@ data) { + int id = -1; + if(data !is null) + id = data.id; + file << id; + } + void postLoad(AI& ai) { + loadIds.length = 0; + } + + const Design@ get_currentSupport() { + for(int i = automatic.length - 1; i >= 0; --i) { + if(automatic[i].purpose == DP_Support && automatic[i].active !is null) + return automatic[i].active; + } + return null; + } + + void getSupportAverages(double& hp, double& dps, double& supDrain) { + hp = 0; + dps = 0; + supDrain = 0; + uint count = 0; + for(uint i = 0, cnt = automatic.length; i < cnt; ++i) { + auto@ targ = automatic[i]; + if(targ.purpose != DP_Support) + continue; + if(targ.active is null) + continue; + + hp += targ.hp / double(targ.targetSize); + dps += targ.dps / double(targ.targetSize); + supDrain += targ.supplyDrain / double(targ.targetSize); + count += 1; + } + + if(count == 0) { + hp = 40.0; + dps = 0.30; + supDrain = 1.0; + } + else { + hp /= double(count); + dps /= double(count); + supDrain /= double(count); + } + } + + DesignPurpose classify(Object@ obj) { + if(obj is null || !obj.isShip) + return DP_Combat; + Ship@ ship = cast(obj); + return classify(ship.blueprint.design); + } + + DesignPurpose classify(const Design@ dsg, DesignPurpose defaultPurpose = DP_Combat) { + if(dsg is null) + return defaultPurpose; + + for(uint i = 0, cnt = automatic.length; i < cnt; ++i) { + if(automatic[i].contains(dsg)) + return DesignPurpose(automatic[i].purpose); + } + + for(uint i = 0, cnt = completed.length; i < cnt; ++i) { + if(completed[i].contains(dsg)) + return DesignPurpose(completed[i].purpose); + } + + if(dsg.hasTag(ST_Mothership)) + return DP_Mothership; + if(dsg.hasTag(ST_Gate)) + return DP_Gate; + if(dsg.hasTag(ST_Slipstream)) + return DP_Slipstream; + if(dsg.hasTag(ST_Support)) + return DP_Support; + if(dsg.hasTag(ST_Station)) + return DP_Defense; + + double dps = dsg.total(SV_DPS); + if(dsg.total(SV_MiningRate) > 0) + return DP_Miner; + if(dsg.size == 16.0 && dsg.total(SV_DPS) < 2.0) + return DP_Scout; + if(dps > 0.1 * dsg.size || dsg.total(SV_SupportCapacity) > 0) + return DP_Combat; + return defaultPurpose; + } + + DesignTarget@ design(uint purpose, int size, int targetCost = 0, int targetMaint = 0, double targetLabor = 0, bool findSize = false) { + for(uint i = 0, cnt = automatic.length; i < cnt; ++i) { + auto@ target = automatic[i]; + if(target.purpose != purpose) + continue; + if(target.targetSize != size) + continue; + if(targetCost != 0 && target.targetBuildCost > targetCost) + continue; + if(targetMaint != 0 && target.targetMaintenance > targetMaint) + continue; + if(targetLabor != 0 && target.targetLaborCost > targetLabor) + continue; + if(target.findSize != findSize) + continue; + return target; + } + + DesignTarget targ(purpose, size); + targ.findSize = findSize; + targ.targetBuildCost = targetCost; + targ.targetMaintenance = targetMaint; + targ.targetLaborCost = targetLabor; + + automatic.insertLast(targ); + return design(targ); + } + + DesignTarget@ design(DesignTarget@ target) { + target.id = nextTargetId++; + designing.insertLast(target); + return target; + } + + DesignTarget@ get(const Design@ dsg) { + for(uint i = 0, cnt = automatic.length; i < cnt; ++i) { + if(automatic[i].contains(dsg)) + return automatic[i]; + } + return null; + } + + DesignTarget@ scale(const Design@ dsg, int newSize) { + if(dsg.newer !is null) { + auto@ newTarg = get(dsg.newest()); + if(newTarg.targetSize == newSize) + return newTarg; + @dsg = dsg.newest(); + } + + DesignTarget@ previous = get(dsg); + + uint purpose = DP_Combat; + if(previous !is null) + purpose = previous.purpose; + else + purpose = classify(dsg); + + DesignTarget target(purpose, newSize); + target.id = nextTargetId++; + @target.active = scaleDesign(dsg, newSize); + + ai.empire.changeDesign(dsg, target.active, ai.empire.getDesignClass(dsg.cls.name, true)); + + if(previous !is null) + automatic.remove(previous); + automatic.insertLast(target); + + return target; + } + + uint chkInd = 0; + void tick(double time) { + if(designing.length != 0) { + //chkInd = (chkInd+1) % designing.length; + // Getting 1 design first is better than getting all of them later + chkInd = 0; + auto@ target = designing[chkInd]; + target.step(ai, this); + + if(target.active !is null) { + designing.removeAt(chkInd); + if(automatic.find(target) == -1) + completed.insertLast(target); + } + } + } +}; + +AIComponent@ createDesigns() { + return Designs(); +} ADDED scripts/server/empire_ai/weasel/Development.as Index: scripts/server/empire_ai/weasel/Development.as ================================================================== --- scripts/server/empire_ai/weasel/Development.as +++ scripts/server/empire_ai/weasel/Development.as @@ -0,0 +1,808 @@ +import empire_ai.weasel.WeaselAI; +import empire_ai.weasel.Planets; +import empire_ai.weasel.Resources; +import empire_ai.weasel.Colonization; +import empire_ai.weasel.Systems; + +import planet_levels; +import buildings; + +import ai.consider; +from ai.buildings import Buildings, BuildingAI, RegisterForLaborUse, AsCreatedResource, BuildingUse; +from ai.resources import AIResources, ResourceAI; + +interface RaceDevelopment { + bool shouldBeFocus(Planet& pl, const ResourceType@ resource); +}; + +class DevelopmentFocus { + Object@ obj; + PlanetAI@ plAI; + int targetLevel = 0; + int requestedLevel = 0; + int maximumLevel = INT_MAX; + array managedPressure; + double weight = 1.0; + + void tick(AI& ai, Development& dev, double time) { + if(targetLevel != requestedLevel) { + if(targetLevel > requestedLevel) { + int nextLevel = min(targetLevel, min(obj.resourceLevel, requestedLevel)+1); + if(nextLevel != requestedLevel) { + for(int i = requestedLevel+1; i <= nextLevel; ++i) + dev.resources.organizeImports(obj, i); + requestedLevel = nextLevel; + } + } + else { + dev.resources.organizeImports(obj, targetLevel); + requestedLevel = targetLevel; + } + } + + //Remove managed pressure resources that are no longer valid + for(uint i = 0, cnt = managedPressure.length; i < cnt; ++i) { + ExportData@ res = managedPressure[i]; + if(res.request !is null || res.obj is null || !res.obj.valid || res.obj.owner !is ai.empire || !res.usable || res.developUse !is obj) { + if(res.developUse is obj) + @res.developUse = null; + managedPressure.removeAt(i); + --i; --cnt; + } + } + + //Make sure we're not exporting our resource + if(plAI !is null && plAI.resources !is null && plAI.resources.length != 0) { + auto@ res = plAI.resources[0]; + res.localOnly = true; + if(res.request !is null && res.request.obj !is res.obj) + dev.resources.breakImport(res); + } + + //TODO: We should be able to bump managed pressure resources back to Development for + //redistribution if we run out of pressure capacity. + } + + void save(Development& development, SaveFile& file) { + file << obj; + development.planets.saveAI(file, plAI); + file << targetLevel; + file << requestedLevel; + file << maximumLevel; + file << weight; + + uint cnt = managedPressure.length; + file << cnt; + for(uint i = 0; i < cnt; ++i) + development.resources.saveExport(file, managedPressure[i]); + } + + void load(Development& development, SaveFile& file) { + file >> obj; + @plAI = development.planets.loadAI(file); + file >> targetLevel; + file >> requestedLevel; + file >> maximumLevel; + file >> weight; + + uint cnt = 0; + file >> cnt; + for(uint i = 0; i < cnt; ++i) { + auto@ data = development.resources.loadExport(file); + managedPressure.insertLast(data); + } + } +}; + +class Development : AIComponent, Buildings, ConsiderFilter, AIResources { + RaceDevelopment@ race; + Planets@ planets; + Resources@ resources; + Colonization@ colonization; + Systems@ systems; + + array focuses; + array managedPressure; + + array pendingFocuses; + array pendingResources; + + array genericBuilds; + array aiResources; + + double aimFTLStorage = 0.0; + double aimResearchRate = 0.0; + + bool managePlanetPressure = true; + bool manageAsteroidPressure = true; + bool buildBuildings = true; + bool colonizeResources = true; + + void create() { + @planets = cast(ai.planets); + @resources = cast(ai.resources); + @colonization = cast(ai.colonization); + @systems = cast(ai.systems); + @race = cast(ai.race); + + //Register specialized building types + for(uint i = 0, cnt = getBuildingTypeCount(); i < cnt; ++i) { + auto@ type = getBuildingType(i); + for(uint n = 0, ncnt = type.ai.length; n < ncnt; ++n) { + auto@ hook = cast(type.ai[n]); + if(hook !is null) + hook.register(this, type); + } + } + } + + Empire@ get_empire() { + return ai.empire; + } + + Considerer@ get_consider() { + return cast(ai.consider); + } + + void registerUse(BuildingUse use, const BuildingType& type) { + switch(use) { + case BU_Factory: + @ai.defs.Factory = type; + break; + case BU_LaborStorage: + @ai.defs.LaborStorage = type; + break; + } + } + + void save(SaveFile& file) { + file << aimFTLStorage; + + uint cnt = focuses.length; + file << cnt; + for(uint i = 0; i < cnt; ++i) { + auto@ focus = focuses[i]; + focus.save(this, file); + } + + cnt = managedPressure.length; + file << cnt; + for(uint i = 0; i < cnt; ++i) + resources.saveExport(file, managedPressure[i]); + + cnt = pendingFocuses.length; + file << cnt; + for(uint i = 0; i < cnt; ++i) + colonization.saveColonize(file, pendingFocuses[i]); + + cnt = pendingResources.length; + file << cnt; + for(uint i = 0; i < cnt; ++i) + colonization.saveColonize(file, pendingResources[i]); + + cnt = genericBuilds.length; + file << cnt; + for(uint i = 0; i < cnt; ++i) + planets.saveBuildingRequest(file, genericBuilds[i]); + + cnt = aiResources.length; + file << cnt; + for(uint i = 0; i < cnt; ++i) + resources.saveExport(file, aiResources[i]); + } + + void load(SaveFile& file) { + file >> aimFTLStorage; + + uint cnt = 0; + file >> cnt; + for(uint i = 0; i < cnt; ++i) { + auto@ focus = DevelopmentFocus(); + focus.load(this, file); + + if(focus.obj !is null) + focuses.insertLast(focus); + } + + file >> cnt; + for(uint i = 0; i < cnt; ++i) { + auto@ data = resources.loadExport(file); + if(data !is null) + managedPressure.insertLast(data); + } + + file >> cnt; + for(uint i = 0; i < cnt; ++i) { + auto@ data = colonization.loadColonize(file); + if(data !is null) + pendingFocuses.insertLast(data); + } + + file >> cnt; + for(uint i = 0; i < cnt; ++i) { + auto@ data = colonization.loadColonize(file); + if(data !is null) + pendingResources.insertLast(data); + } + + file >> cnt; + for(uint i = 0; i < cnt; ++i) { + auto@ data = planets.loadBuildingRequest(file); + if(data !is null) + genericBuilds.insertLast(data); + } + + file >> cnt; + for(uint i = 0; i < cnt; ++i) { + auto@ data = resources.loadExport(file); + if(data !is null) + aiResources.insertLast(data); + } + } + + bool requestsFTLStorage() { + double capacity = ai.empire.FTLCapacity; + if(aimFTLStorage <= capacity) + return false; + if(ai.empire.FTLStored < capacity * 0.5) + return false; + return true; + } + + bool requestsResearchGeneration() { + double rate = ai.empire.ResearchRate; + if (aimResearchRate <= rate) + return false; + return true; + } + + bool isBuilding(const BuildingType& type) { + for(uint i = 0, cnt = genericBuilds.length; i < cnt; ++i) { + if(genericBuilds[i].type is type) + return true; + } + return false; + } + + bool isLeveling() { + for(uint i = 0, cnt = focuses.length; i < cnt; ++i) { + if(focuses[i].obj.resourceLevel < uint(focuses[i].targetLevel)) { + auto@ focus = focuses[i].obj; + + //If all our requirements are resolved, then we can safely assume it will be leveled up + bool allResolved = true; + for(uint n = 0, ncnt = resources.requested.length; n < ncnt; ++n) { + auto@ req = resources.requested[n]; + if(req.obj !is focus) + continue; + if(req.beingMet) + continue; + + if(!req.isColonizing) { + allResolved = false; + break; + } + + if(!colonization.isResolved(req)) { + allResolved = false; + break; + } + } + + if(!allResolved) + return true; + } + } + return false; + } + + bool isBusy() { + if(pendingFocuses.length != 0) + return true; + if(pendingResources.length != 0) + return true; + if(isLeveling()) + return true; + return false; + } + + bool isFocus(Object@ obj) { + for(uint i = 0, cnt = focuses.length; i < cnt; ++i) { + if(focuses[i].obj is obj) + return true; + } + return false; + } + + bool isManaging(ExportData@ res) { + for(uint i = 0, cnt = managedPressure.length; i < cnt; ++i) { + if(managedPressure[i] is res) + return true; + } + for(uint i = 0, cnt = aiResources.length; i < cnt; ++i) { + if(aiResources[i] is res) + return true; + } + for(uint n = 0, ncnt = focuses.length; n < ncnt; ++n) { + auto@ f = focuses[n]; + if(f.obj is res.obj) + return true; + for(uint i = 0, cnt = f.managedPressure.length; i < cnt; ++i) { + if(f.managedPressure[i] is res) + return true; + } + } + return false; + } + + bool isDevelopingIn(Region@ reg) { + if(reg is null) + return false; + for(uint i = 0, cnt = focuses.length; i < cnt; ++i) { + if(focuses[i].obj.region is reg) + return true; + } + return false; + } + + void start() { + //Level up the homeworld to level 3 to start with + for(uint i = 0, cnt = ai.empire.planetCount; i < cnt; ++i) { + Planet@ homeworld = ai.empire.planetList[i]; + if(homeworld !is null && homeworld.valid) { + auto@ hwFocus = addFocus(planets.register(homeworld)); + if(homeworld.nativeResourceCount >= 2 || homeworld.primaryResourceLimitLevel >= 3 || cnt == 1) + hwFocus.targetLevel = 3; + } + } + } + + double idlePenalty = 0; + void findSomethingToDo() { + if(idlePenalty > gameTime) + return; + + double totalChance = + ai.behavior.focusDevelopWeight + + ai.behavior.focusColonizeNewWeight * sqr(1.0 / double(focuses.length)) + + ai.behavior.focusColonizeHighTierWeight; + double roll = randomd(0.0, totalChance); + + //Level up one of our existing focuses + roll -= ai.behavior.focusDevelopWeight; + if(roll <= 0) { + DevelopmentFocus@ levelup; + double totalWeight = 0.0; + for(uint i = 0, cnt = focuses.length; i < cnt; ++i) { + auto@ f = focuses[i]; + if(f.weight == 0) + continue; + if(f.targetLevel >= f.maximumLevel) + continue; + totalWeight += f.weight; + if(randomd() < f.weight / totalWeight) + @levelup = f; + } + + if(levelup !is null) { + levelup.targetLevel += 1; + if(log) + ai.print("Develop chose to level this up to "+levelup.targetLevel, levelup.obj); + return; + } + else { + if(log) + ai.print("Develop ran out of things to level up."); + } + } + + if(!colonizeResources) + return; + + //Find a scalable or high tier resource to colonize and turn into a focus + roll -= ai.behavior.focusColonizeNewWeight * sqr(1.0 / double(focuses.length)); + if(roll <= 0) { + Planet@ newFocus; + double w; + double bestWeight = 0.0; + + for(uint i = 0, cnt = colonization.potentials.length; i < cnt; ++i) { + auto@ p = colonization.potentials[i]; + + if(p.resource.level < 3 && p.resource.cls !is colonization.scalableClass) + continue; + + Region@ reg = p.pl.region; + if(reg is null) + continue; + + if(colonization.isColonizing(p.pl)) + continue; + + vec2i surfaceSize = p.pl.surfaceGridSize; + int tiles = surfaceSize.width * surfaceSize.height; + if(tiles < 144) + continue; + + auto@ sys = systems.getAI(reg); + w = 1.0; + if(sys.border) + w *= 0.25; + if (!sys.owned && !sys.border) + w /= 0.25; + if(sys.obj.PlanetsMask & ~ai.mask != 0) + w *= 0.25; + if(p.resource.cls is colonization.scalableClass) + w *= 10.0; + + if (w > bestWeight) { + @newFocus = p.pl; + bestWeight = w; + } + } + + if(newFocus !is null) { + auto@ data = colonization.colonize(newFocus); + if(data !is null) + pendingFocuses.insertLast(data); + if(log) + ai.print("Colonize to become develop focus", data.target); + return; + } + else { + if(log) + ai.print("Develop could not find a scalable or high tier resource to make a focus."); + } + } + + if(focuses.length == 0) + return; + + //Find a high tier resource to import to one of our focuses + roll -= ai.behavior.focusColonizeHighTierWeight; + if(roll <= 0) { + ResourceSpec spec; + spec.type = RST_Level_Minimum; + spec.level = 3; + spec.isLevelRequirement = false; + + auto@ data = colonization.colonize(spec); + if(data !is null) { + if(log) + ai.print("Colonize as free resource", data.target); + pendingResources.insertLast(data); + return; + } + else { + if(log) + ai.print("Develop could not find a high tier resource to colonize as free resource."); + } + } + + //Try to find a level 2 resource if everything else failed + { + ResourceSpec spec; + spec.type = RST_Level_Minimum; + spec.level = 2; + spec.isLevelRequirement = false; + + if(colonization.shouldQueueFor(spec)) { + auto@ data = colonization.colonize(spec); + if(data !is null) { + if(log) + ai.print("Colonize as free resource", data.target); + pendingResources.insertLast(data); + return; + } + else { + if(log) + ai.print("Develop could not find a level 2 resource to colonize as free resource."); + } + } + } + + idlePenalty = gameTime + randomd(10.0, 40.0); + } + + uint bldIndex = 0; + uint aiInd = 0; + uint presInd = 0; + uint chkInd = 0; + void focusTick(double time) override { + //Remove any resources we're managing that got used + for(uint i = 0, cnt = managedPressure.length; i < cnt; ++i) { + ExportData@ res = managedPressure[i]; + if(res.request !is null || res.obj is null || !res.obj.valid || res.obj.owner !is ai.empire || !res.usable) { + managedPressure.removeAt(i); + --i; --cnt; + } + } + + //Find new resources that we can put in our pressure manager + uint avCnt = resources.available.length; + if(avCnt != 0) { + uint index = randomi(0, avCnt-1); + for(uint i = 0, cnt = min(avCnt, 3); i < cnt; ++i) { + uint resInd = (index+i) % avCnt; + ExportData@ res = resources.available[resInd]; + if(res.usable && res.request is null && res.obj !is null && res.obj.valid && res.obj.owner is ai.empire && res.developUse is null) { + if(res.resource.ai.length != 0) { + if(!isManaging(res)) + aiResources.insertLast(res); + } + else if(res.resource.totalPressure > 0 && res.resource.exportable) { + if(!managePlanetPressure && res.obj.isPlanet) + continue; + if(!manageAsteroidPressure && res.obj.isAsteroid) + continue; + if(!isManaging(res)) + managedPressure.insertLast(res); + } + } + } + } + + //Distribute managed pressure resources + if(managedPressure.length != 0) { + presInd = (presInd+1) % managedPressure.length; + ExportData@ res = managedPressure[presInd]; + + int pressure = res.resource.totalPressure; + + DevelopmentFocus@ onFocus; + double bestWeight = 0; + bool havePressure = ai.empire.HasPressure != 0.0; + + for(uint i = 0, cnt = focuses.length; i < cnt; ++i) { + auto@ f = focuses[i]; + + int cap = f.obj.pressureCap; + if(!havePressure) + cap = 10000; + int cur = f.obj.totalPressure; + + if(cur + pressure > 2 * cap) + continue; + + double w = 1.0; + if(cur + pressure > cap) + w *= 0.1; + + if(w > bestWeight) { + bestWeight = w; + @onFocus = f; + } + } + + if(onFocus !is null) { + if(res.obj !is onFocus.obj) + res.obj.exportResourceByID(res.resourceId, onFocus.obj); + else + res.obj.exportResourceByID(res.resourceId, null); + @res.developUse = onFocus.obj; + + onFocus.managedPressure.insertLast(res); + managedPressure.removeAt(presInd); + + if(log) + ai.print("Take "+res.resource.name+" from "+res.obj.name+" for pressure", onFocus.obj); + } + } + + //Use generic AI distribution hooks + if(aiResources.length != 0) { + aiInd = (aiInd+1) % aiResources.length; + ExportData@ res = aiResources[aiInd]; + if(res.request !is null || res.obj is null || !res.obj.valid || res.obj.owner !is ai.empire || !res.usable) { + aiResources.removeAt(aiInd); + } + else { + Object@ newTarget = res.developUse; + if(newTarget !is null) { + if(!newTarget.valid || newTarget.owner !is ai.empire) + @newTarget = null; + } + + for(uint i = 0, cnt = res.resource.ai.length; i < cnt; ++i) { + auto@ hook = cast(res.resource.ai[i]); + if(hook !is null) + @newTarget = hook.distribute(this, res.resource, newTarget); + } + + if(newTarget !is res.developUse) { + if(res.obj !is newTarget) + res.obj.exportResourceByID(res.resourceId, newTarget); + else + res.obj.exportResourceByID(res.resourceId, null); + @res.developUse = newTarget; + } + } + } + + //Deal with focuses we're colonizing + for(uint i = 0, cnt = pendingFocuses.length; i < cnt; ++i) { + auto@ data = pendingFocuses[i]; + if(data.completed) { + auto@ focus = addFocus(planets.register(data.target)); + focus.targetLevel = 3; + + pendingFocuses.removeAt(i); + --i; --cnt; + } + else if(data.canceled) { + pendingFocuses.removeAt(i); + --i; --cnt; + } + } + + for(uint i = 0, cnt = pendingResources.length; i < cnt; ++i) { + auto@ data = pendingResources[i]; + if(data.completed) { + planets.requestLevel(planets.register(data.target), data.target.primaryResourceLevel); + pendingResources.removeAt(i); + --i; --cnt; + } + else if(data.canceled) { + pendingResources.removeAt(i); + --i; --cnt; + } + } + + //If we're not currently leveling something up, find something else to do + if(!isBusy()) + findSomethingToDo(); + + //Deal with building AI hints + for(uint i = 0, cnt = genericBuilds.length; i < cnt; ++i) { + auto@ build = genericBuilds[i]; + if(build.canceled) { + genericBuilds.removeAt(i); + --i; --cnt; + } + else if(build.built) { + if(build.getProgress() >= 1.f) { + if(build.expires < gameTime) { + genericBuilds.removeAt(i); + --i; --cnt; + } + } + else + build.expires = gameTime + 60.0; + } + } + if(buildBuildings) { + for(uint i = 0, cnt = getBuildingTypeCount(); i < cnt; ++i) { + bldIndex = (bldIndex+1) % cnt; + + auto@ type = getBuildingType(bldIndex); + if(type.ai.length == 0) + continue; + + //If we're already generically building something of this type, wait + bool existing = false; + for(uint n = 0, ncnt = genericBuilds.length; n < ncnt; ++n) { + auto@ build = genericBuilds[n]; + if(build.type is type && !build.built) { + existing = true; + break; + } + } + + if(existing) + break; + + @filterType = type; + @consider.filter = this; + + //See if we should generically build something of this type + for(uint n = 0, ncnt = type.ai.length; n < ncnt; ++n) { + auto@ hook = cast(type.ai[n]); + if(hook !is null) { + Object@ buildOn = hook.considerBuild(this, type); + if(buildOn !is null && buildOn.isPlanet) { + auto@ plAI = planets.getAI(cast(buildOn)); + if(plAI !is null) { + if(log) + ai.print("AI hook generically requested building of type "+type.name, buildOn); + + double priority = 1.0; + //Resource buildings should be built as soon as possible + if (cast(hook) !is null) + priority = 2.0; + + auto@ req = planets.requestBuilding(plAI, type, priority, expire=ai.behavior.genericBuildExpire); + if(req !is null) + genericBuilds.insertLast(req); + break; + } + } + } + } + break; + } + } + + //Find planets we've acquired 'somehow' that have scalable resources and should be development focuses + if(planets.planets.length != 0) { + chkInd = (chkInd+1) % planets.planets.length; + auto@ plAI = planets.planets[chkInd]; + + if(plAI.resources.length != 0) { + auto@ res = plAI.resources[0]; + if(res.resource.cls is colonization.scalableClass + || focuses.length == 0 && res.resource.level >= 2 + || (race !is null && race.shouldBeFocus(plAI.obj, res.resource))) { + if(!isFocus(plAI.obj)) { + auto@ focus = addFocus(plAI); + focus.targetLevel = max(1, res.resource.level); + } + } + } + } + } + + DevelopmentFocus@ addFocus(PlanetAI@ plAI) { + DevelopmentFocus focus; + @focus.obj = plAI.obj; + @focus.plAI = plAI; + focus.maximumLevel = getMaxPlanetLevel(plAI.obj); + + focuses.insertLast(focus); + return focus; + } + + DevelopmentFocus@ getFocus(Planet& pl) { + for(uint i = 0, cnt = focuses.length; i < cnt; ++i) { + if(focuses[i].obj is pl) + return focuses[i]; + } + return null; + } + + void tick(double time) override { + for(uint i = 0, cnt = focuses.length; i < cnt; ++i) + focuses[i].tick(ai, this, time); + } + + const BuildingType@ filterType; + bool filter(Object@ obj) { + for(uint i = 0, cnt = genericBuilds.length; i < cnt; ++i) { + auto@ build = genericBuilds[i]; + if(build.type is filterType && build.plAI.obj is obj) + return false; + } + return true; + } + + Planet@ getLaborAt(Territory@ territory, double&out expires) { + if (territory is null) { + if (log) + ai.print("invalid territory to get labor at"); + return null; + } + expires = 600.0; + const BuildingType@ type = ai.defs.Factory; + BuildingRequest@ request = null; + Planet@ pl = null; + for (uint i = 0, cnt = type.ai.length; i < cnt; ++i) { + auto@ hook = cast(type.ai[i]); + if (hook !is null) { + Object@ obj = hook.considerBuild(this, type, territory); + if (obj !is null) { + @pl = cast(obj); + if (pl !is null) { + planets.requestBuilding(planets.getAI(pl), type, 2.0, expires); + if (log) + ai.print("requesting building " + type.name + " at " + pl.name + " to get labor at " + addrstr(territory)); + break; + } + } + } + } + return pl; + } +}; + +AIComponent@ createDevelopment() { + return Development(); +} ADDED scripts/server/empire_ai/weasel/Diplomacy.as Index: scripts/server/empire_ai/weasel/Diplomacy.as ================================================================== --- scripts/server/empire_ai/weasel/Diplomacy.as +++ scripts/server/empire_ai/weasel/Diplomacy.as @@ -0,0 +1,396 @@ +// Diplomacy +// --------- +// Acts as an adaptor for using the generically developed DiplomacyAI. +// + +import empire_ai.weasel.WeaselAI; +import empire_ai.weasel.Systems; +import empire_ai.weasel.Planets; +import empire_ai.weasel.Development; +import empire_ai.weasel.Construction; +import empire_ai.weasel.Fleets; +import empire_ai.weasel.Resources; +import empire_ai.weasel.War; +import empire_ai.weasel.Intelligence; + +import influence; +from empire_ai.DiplomacyAI import DiplomacyAI, VoteData, CardAI, VoteState; + +import systems; + +class Diplomacy : DiplomacyAI, IAIComponent { + Systems@ systems; + Fleets@ fleets; + Planets@ planets; + Construction@ construction; + Development@ development; + Resources@ resources; + War@ war; + Intelligence@ intelligence; + + //Adapt to AI component + AI@ ai; + double prevFocus = 0; + bool logCritical = false; + bool logErrors = true; + + double getPrevFocus() { return prevFocus; } + void setPrevFocus(double value) { prevFocus = value; } + + void setLog() { log = true; } + void setLogCritical() { logCritical = true; } + + void set(AI& ai) { @this.ai = ai; } + void start() {} + + void tick(double time) {} + void turn() {} + + void postLoad(AI& ai) {} + void postSave(AI& ai) {} + void loadFinalize(AI& ai) {} + + //Actual AI component implementations + void create() { + @systems = cast(ai.systems); + @fleets = cast(ai.fleets); + @planets = cast(ai.planets); + @development = cast(ai.development); + @construction = cast(ai.construction); + @resources = cast(ai.resources); + @war = cast(ai.war); + @intelligence = cast(ai.intelligence); + } + + //IMPLEMENTED BY DiplomacyAI + /*void save(SaveFile& file) {}*/ + /*void load(SaveFile& file) {}*/ + + uint nextStep = 0; + void focusTick(double time) { + summarize(); + + if (ai.behavior.forbidDiplomacy) return; + switch(nextStep++ % 3) { + case 0: + buyCards(); + break; + case 1: + considerActions(); + break; + case 2: + considerVotes(); + break; + } + } + + //Adapt to diplomacy AI + Empire@ get_empire() { + return ai.empire; + } + + uint get_allyMask() { + return ai.allyMask; + } + + int getStanding(Empire@ emp) { + //TODO: Use relations module for this generically + if(emp.isHostile(ai.empire)) + return -50; + if(ai.allyMask & emp.mask != 0) + return 100; + return 0; + } + + void print(const string& str) { + ai.print(str); + } + + Object@ considerImportantPlanets(const CardAI& hook, Targets& targets, VoteState@ vote = null, const InfluenceCard@ card = null) { + double bestWeight = 0.0; + Object@ best; + + for(uint i = 0, cnt = development.focuses.length; i < cnt; ++i) { + Object@ obj = development.focuses[i].obj; + double w = hook.consider(this, targets, vote, card, obj); + if(w > bestWeight) { + @best = obj; + bestWeight = w; + } + } + + return best; + } + + Object@ considerOwnedPlanets(const CardAI& hook, Targets& targets, VoteState@ vote = null, const InfluenceCard@ card = null) { + //Consider our important ones first + double bestWeight = 0.0; + Object@ best; + + for(uint i = 0, cnt = development.focuses.length; i < cnt; ++i) { + Object@ obj = development.focuses[i].obj; + double w = hook.consider(this, targets, vote, card, obj); + if(w > bestWeight) { + @best = obj; + bestWeight = w; + } + } + + //Consider some random other ones + uint planetCount = planets.planets.length; + if(planetCount != 0) { + uint offset = randomi(0, planetCount-1); + for(uint n = 0; n < 5; ++n) { + Object@ obj = planets.planets[(offset+n) % planetCount].obj; + double w = hook.consider(this, targets, vote, card, obj); + if(w > bestWeight) { + @best = obj; + bestWeight = w; + } + } + } + + return best; + } + + Object@ considerImportantSystems(const CardAI& hook, Targets& targets, VoteState@ vote = null, const InfluenceCard@ card = null) { + double bestWeight = 0.0; + Object@ best; + + for(uint i = 0, cnt = development.focuses.length; i < cnt; ++i) { + Object@ obj = development.focuses[i].obj.region; + if(obj is null) + continue; + double w = hook.consider(this, targets, vote, card, obj); + if(w > bestWeight) { + @best = obj; + bestWeight = w; + } + } + + return best; + } + + Object@ considerOwnedSystems(const CardAI& hook, Targets& targets, VoteState@ vote = null, const InfluenceCard@ card = null) { + //Consider our important ones first + double bestWeight = 0.0; + Object@ best; + + for(uint i = 0, cnt = development.focuses.length; i < cnt; ++i) { + Object@ obj = development.focuses[i].obj.region; + if(obj is null) + continue; + double w = hook.consider(this, targets, vote, card, obj); + if(w > bestWeight) { + @best = obj; + bestWeight = w; + } + } + + //Consider some random other ones + uint sysCount = systems.owned.length; + if(sysCount != 0) { + uint offset = randomi(0, sysCount-1); + for(uint n = 0; n < 5; ++n) { + Object@ obj = systems.owned[(offset+n) % sysCount].obj; + double w = hook.consider(this, targets, vote, card, obj); + if(w > bestWeight) { + @best = obj; + bestWeight = w; + } + } + } + + return best; + } + + Object@ considerDefendingSystems(const CardAI& hook, Targets& targets, VoteState@ vote = null, const InfluenceCard@ card = null) { + double bestWeight = 0.0; + Object@ best; + + for(uint i = 0, cnt = war.battles.length; i < cnt; ++i) { + auto@ battle = war.battles[i]; + Region@ sys = battle.system.obj; + if(sys.SiegedMask & empire.mask == 0) + continue; + + double w = hook.consider(this, targets, vote, card, sys); + if(w > bestWeight) { + @best = sys; + bestWeight = w; + } + } + + return best; + } + + Object@ considerDefendingPlanets(const CardAI& hook, Targets& targets, VoteState@ vote = null, const InfluenceCard@ card = null) { + double bestWeight = 0.0; + Object@ best; + + for(uint i = 0, cnt = war.battles.length; i < cnt; ++i) { + auto@ battle = war.battles[i]; + Region@ sys = battle.system.obj; + if(sys.SiegedMask & empire.mask == 0) + continue; + + for(uint n = 0, ncnt = battle.system.planets.length; n < ncnt; ++n) { + Object@ pl = battle.system.planets[n]; + if(pl.owner !is empire) + continue; + + double w = hook.consider(this, targets, vote, card, pl); + if(w > bestWeight) { + @best = pl; + bestWeight = w; + } + } + } + + return best; + } + + Object@ considerEnemySystems(const CardAI& hook, Targets& targets, VoteState@ vote = null, const InfluenceCard@ card = null) { + double bestWeight = 0.0; + Object@ best; + + for(uint i = 0, cnt = getEmpireCount(); i < cnt; ++i) { + Empire@ emp = getEmpire(i); + if(!emp.major) + continue; + if(!empire.isHostile(emp)) + continue; + + auto@ intel = intelligence.get(emp); + if(intel is null) + continue; + + for(uint n = 0, ncnt = intel.theirBorder.length; n < ncnt; ++n) { + auto@ sysIntel = intel.theirBorder[n]; + + double w = hook.consider(this, targets, vote, card, sysIntel.obj); + if(w > bestWeight) { + @best = sysIntel.obj; + bestWeight = w; + } + } + } + + return best; + } + + Object@ considerEnemyPlanets(const CardAI& hook, Targets& targets, VoteState@ vote = null, const InfluenceCard@ card = null) { + double bestWeight = 0.0; + Object@ best; + + for(uint i = 0, cnt = getEmpireCount(); i < cnt; ++i) { + Empire@ emp = getEmpire(i); + if(!emp.major) + continue; + if(!empire.isHostile(emp)) + continue; + + auto@ intel = intelligence.get(emp); + if(intel is null) + continue; + + for(uint n = 0, ncnt = intel.theirBorder.length; n < ncnt; ++n) { + auto@ sysIntel = intel.theirBorder[n]; + + for(uint j = 0, jcnt = sysIntel.planets.length; j < jcnt; ++j) { + Planet@ pl = sysIntel.planets[j]; + if(pl.visibleOwnerToEmp(empire) !is emp) + continue; + + double w = hook.consider(this, targets, vote, card, pl); + if(w > bestWeight) { + @best = pl; + bestWeight = w; + } + } + } + } + + return best; + } + + Object@ considerFleets(const CardAI& hook, Targets& targets, VoteState@ vote = null, const InfluenceCard@ card = null) { + Object@ best; + double bestWeight = 0.0; + for(uint i = 0, cnt = fleets.fleets.length; i < cnt; ++i) { + Object@ fleet = fleets.fleets[i].obj; + if(fleet !is null) { + double w = hook.consider(this, targets, vote, card, fleet); + if(w > bestWeight) { + @best = fleet; + bestWeight = w; + } + } + } + return best; + } + + Object@ considerEnemyFleets(const CardAI& hook, Targets& targets, VoteState@ vote = null, const InfluenceCard@ card = null) { + double bestWeight = 0.0; + Object@ best; + + for(uint i = 0, cnt = getEmpireCount(); i < cnt; ++i) { + Empire@ emp = getEmpire(i); + if(!emp.major) + continue; + if(!empire.isHostile(emp)) + continue; + + auto@ intel = intelligence.get(emp); + if(intel is null) + continue; + + for(uint n = 0, ncnt = intel.fleets.length; n < ncnt; ++n) { + auto@ flIntel = intel.fleets[n]; + if(!flIntel.known) + continue; + + double w = hook.consider(this, targets, vote, card, flIntel.obj); + if(w > bestWeight) { + @best = flIntel.obj; + bestWeight = w; + } + } + } + + return best; + } + + Object@ considerMatchingImportRequests(const CardAI& hook, Targets& targets, VoteState@ vote, const InfluenceCard@ card, const ResourceType@ type, bool considerExisting) { + Object@ best; + double bestWeight = 0.0; + for(uint i = 0, cnt = resources.requested.length; i < cnt; ++i) { + ImportData@ req = resources.requested[i]; + if(req.spec.meets(type)) { + double w = hook.consider(this, targets, vote, card, req.obj, null); + if(w > bestWeight) { + bestWeight = w; + @best = req.obj; + } + } + } + if(considerExisting) { + for(uint i = 0, cnt = resources.used.length; i < cnt; ++i) { + ExportData@ res = resources.used[i]; + ImportData@ req = res.request; + if(req !is null && req.spec.meets(type)) { + double w = hook.consider(this, targets, vote, card, req.obj, res.obj); + if(w > bestWeight) { + bestWeight = w; + @best = req.obj; + } + } + } + } + return best; + } +}; + +IAIComponent@ createDiplomacy() { + return Diplomacy(); +} ADDED scripts/server/empire_ai/weasel/Energy.as Index: scripts/server/empire_ai/weasel/Energy.as ================================================================== --- scripts/server/empire_ai/weasel/Energy.as +++ scripts/server/empire_ai/weasel/Energy.as @@ -0,0 +1,382 @@ +// Energy +// ------ +// Manage the use of energy on artifacts and other things. +// + +import empire_ai.weasel.WeaselAI; +import empire_ai.weasel.Systems; + +import ai.consider; + +import artifacts; +import abilities; +import systems; + +from ai.artifacts import Artifacts, ArtifactConsider, ArtifactAI; + +double effCostEstimate(double cost, double freeStorage) { + double free = min(cost, freeStorage); + cost -= free; + + double effStep = config::ENERGY_EFFICIENCY_STEP; + double eff = 0.0; + double step = 1.0; + while(cost > 0) { + eff += (cost / effStep) * step; + cost -= effStep; + step *= 2.0; + } + return eff * effStep + free; +} + +class ConsiderEnergy : ArtifactConsider { + int id = -1; + const ArtifactType@ type; + Artifact@ artifact; + Ability@ ability; + Object@ target; + vec3d pointTarget; + double cost = 0.0; + double value = 0.0; + + void save(AI& ai, SaveFile& file) { + file << id; + file << artifact; + file << target; + file << cost; + file << value; + file << pointTarget; + } + + void load(AI& ai, SaveFile& file) { + file >> id; + file >> artifact; + file >> target; + file >> cost; + file >> value; + file >> pointTarget; + + if(artifact !is null) + init(ai, artifact); + } + + void setTarget(Object@ obj) { + @target = obj; + } + + Object@ getTarget() { + return target; + } + + bool canTarget(Object@ obj) { + if(ability.targets.length != 0) { + auto@ targ = ability.targets[0]; + @targ.obj = obj; + targ.filled = true; + return ability.isValidTarget(0, targ); + } + else + return false; + } + + void setTargetPosition(const vec3d& point) { + pointTarget = point; + } + + vec3d getTargetPosition() { + return pointTarget; + } + + bool canTargetPosition(const vec3d& point) { + if(ability.targets.length != 0) { + auto@ targ = ability.targets[0]; + targ.point = point; + targ.filled = true; + return ability.isValidTarget(0, targ); + } + else + return false; + } + + void init(AI& ai, Artifact@ artifact) { + @this.artifact = artifact; + @type = getArtifactType(artifact.ArtifactType); + + if(ability is null) + @ability = Ability(); + if(type.secondaryChance > 0 && type.abilities.length >= 2 + && randomd() < type.secondaryChance) { + ability.id = 1; + @ability.type = type.abilities[1]; + } + else { + ability.id = 0; + @ability.type = type.abilities[0]; + } + ability.targets = Targets(ability.type.targets); + @ability.obj = artifact; + @ability.emp = ai.empire; + } + + bool isValid(AI& ai, Energy& energy) { + return energy.canUse(artifact); + } + + void considerEnergy(AI& ai, Energy& energy) { + if(type !is null && type.abilities.length != 0) { + value = 1.0; + + for(uint i = 0, cnt = type.ai.length; i < cnt; ++i) { + ArtifactAI@ ai; + if(ability.id == 0) + @ai = cast(type.ai[i]); + else + @ai = cast(type.secondaryAI[i]); + if(ai !is null) { + if(!ai.consider(energy, this, value)) { + value = 0.0; + break; + } + } + } + if(type.ai.length == 0) + value = 0.0; + + if(ability.targets.length != 0) { + if(ability.targets[0].type == TT_Object) { + @ability.targets[0].obj = target; + ability.targets[0].filled = true; + + if(target is null) + value = 0.0; + } + else if(ability.targets[0].type == TT_Point) { + ability.targets[0].point = pointTarget; + ability.targets[0].filled = true; + } + } + + if(value > 0.0) { + if(!ability.canActivate(ability.targets, ignoreCost=true)) { + value = 0.0; + } + else { + cost = ability.getEnergyCost(ability.targets); + if(cost != 0.0) { + //Estimate the amount of turns it would take to trigger this, + //and devalue it based on that. This is ceiled in order to allow + //for artifacts of similar cost to not be affected by cost differences. + double effCost = effCostEstimate(cost, energy.freeStorage); + double estTime = effCost / max(energy.baseIncome, 0.01); + double turns = ceil(estTime / (3.0 * 60.0)); + value /= turns; + } + else { + value *= 1000.0; + } + } + } + } + else { + value = 0.0; + } + } + + void execute(AI& ai, Energy& energy) { + if(artifact !is null && type.abilities.length != 0) { + if(energy.log) + ai.print("Activate artifact "+artifact.name, artifact.region); + + if(ability.type.targets.length != 0) { + if(ability.type.targets[0].type == TT_Object) + artifact.activateAbilityTypeFor(ai.empire, ability.type.id, target); + else if(ability.type.targets[0].type == TT_Point) + artifact.activateAbilityTypeFor(ai.empire, ability.type.id, pointTarget); + } + else { + artifact.activateAbilityTypeFor(ai.empire, ability.type.id); + } + } + } + + int opCmp(const ConsiderEnergy@ other) const { + if(value < other.value) + return -1; + if(value > other.value) + return 1; + return 0; + } +}; + +class Energy : AIComponent, Artifacts { + Systems@ systems; + + double baseIncome; + double freeStorage; + + array queue; + int nextEnergyId = 0; + + void save(SaveFile& file) { + file << nextEnergyId; + + uint cnt = queue.length; + file << cnt; + for(uint i = 0; i < cnt; ++i) + queue[i].save(ai, file); + } + + void load(SaveFile& file) { + file >> nextEnergyId; + + uint cnt = 0; + file >> cnt; + for(uint i = 0; i < cnt; ++i) { + ConsiderEnergy c; + c.load(ai, file); + + if(c.artifact !is null) + queue.insertLast(c); + } + } + + void create() { + @systems = cast(ai.systems); + } + + Considerer@ get_consider() { + return cast(ai.consider); + } + + Empire@ get_empire() { + return ai.empire; + } + + bool canUse(Artifact@ artifact) { + if(artifact is null || !artifact.valid) + return false; + Empire@ owner = artifact.owner; + if(owner.valid && owner !is ai.empire) + return false; + Region@ reg = artifact.region; + if(reg is null) + return false; + if(reg.PlanetsMask != 0) + return reg.PlanetsMask & ai.mask != 0; + else + return hasTradeAdjacent(ai.empire, reg); + } + + ConsiderEnergy@ registerArtifact(Artifact@ artifact) { + if(!canUse(artifact)) + return null; + + for(uint i = 0, cnt = queue.length; i < cnt; ++i) { + if(queue[i].artifact is artifact) + return queue[i]; + } + + ConsiderEnergy c; + c.id = nextEnergyId++; + c.init(ai, artifact); + + if(log) + ai.print("Detect artifact "+artifact.name, artifact.region); + + queue.insertLast(c); + return c; + } + + uint updateIdx = 0; + bool update() { + if(queue.length == 0) + return false; + updateIdx = (updateIdx+1) % queue.length; + auto@ c = queue[updateIdx]; + double prevValue = c.value; + + //Make sure this is still valid + if(!c.isValid(ai, this)) { + queue.removeAt(updateIdx); + return false; + } + + //Update the current target and value + c.considerEnergy(ai, this); + + /*if(log)*/ + /* ai.print(c.artifact.name+": consider "+c.value+" for cost "+c.cost, c.target);*/ + + //Only re-sort when needed + bool changed = false; + if(prevValue != c.value) { + if(updateIdx > 0) { + if(c.value > queue[updateIdx-1].value) + changed = true; + } + if(updateIdx < queue.length-1) { + if(c.value < queue[updateIdx+1].value) + changed = true; + } + } + + return changed; + } + + uint sysIdx = 0; + void updateSystem() { + uint totCnt = systems.owned.length + systems.outsideBorder.length; + if(totCnt == 0) + return; + + sysIdx = (sysIdx+1) % totCnt; + SystemAI@ sys; + if(sysIdx < systems.owned.length) + @sys = systems.owned[sysIdx]; + else + @sys = systems.outsideBorder[sysIdx - systems.owned.length]; + + for(uint i = 0, cnt = sys.artifacts.length; i < cnt; ++i) + registerArtifact(sys.artifacts[i]); + } + + void tick(double time) { + if(ai.behavior.forbidArtifact) return; + //Update current income + baseIncome = empire.EnergyIncome; + freeStorage = empire.FreeEnergyStorage; + + //See if we can use anything right now + if(queue.length != 0) { + auto@ c = queue[0]; + if(!c.isValid(ai, this)) { + queue.removeAt(0); + } + else if(c.value > 0.0 && ai.empire.EnergyStored >= c.cost) { + c.execute(ai, this); + queue.removeAt(0); + } + } + } + + void focusTick(double time) { + if(ai.behavior.forbidArtifact) return; + //Consider artifact usage + bool changed = false; + for(uint n = 0; n < min(queue.length, max(ai.behavior.artifactFocusConsiderCount, queue.length/20)); ++n) { + if(update()) + changed = true; + } + + //Re-sort consideration + if(changed) + queue.sortDesc(); + + //Try to find new artifacts + updateSystem(); + } +}; + +AIComponent@ createEnergy() { + return Energy(); +} ADDED scripts/server/empire_ai/weasel/Events.as Index: scripts/server/empire_ai/weasel/Events.as ================================================================== --- scripts/server/empire_ai/weasel/Events.as +++ scripts/server/empire_ai/weasel/Events.as @@ -0,0 +1,118 @@ +// Events +// ------ +// Notifies subscribed components of events raised by other components. +// +import empire_ai.weasel.WeaselAI; + +import ai.events; + +final class Events : AIComponent { + //Event callbacks + + private array _onOwnedSystemAdded; + private array _onOwnedSystemRemoved; + private array _onBorderSystemAdded; + private array _onBorderSystemRemoved; + private array _onOutsideBorderSystemAdded; + private array _onOutsideBorderSystemRemoved; + private array _onPlanetAdded; + private array _onPlanetRemoved; + private array _onTradeRouteNeeded; + private array _onOrbitalRequested; + + void create() { + } + + //Event delegate registration + + Events@ opAddAssign(IOwnedSystemEvents& events) { + _onOwnedSystemAdded.insertLast(EventHandler(events.onOwnedSystemAdded)); + _onOwnedSystemRemoved.insertLast(EventHandler(events.onOwnedSystemRemoved)); + return this; + } + + Events@ opAddAssign(IBorderSystemEvents& events) { + _onBorderSystemAdded.insertLast(EventHandler(events.onBorderSystemAdded)); + _onBorderSystemRemoved.insertLast(EventHandler(events.onBorderSystemRemoved)); + return this; + } + + Events@ opAddAssign(IOutsideBorderSystemEvents& events) { + _onOutsideBorderSystemAdded.insertLast(EventHandler(events.onOutsideBorderSystemAdded)); + _onOutsideBorderSystemRemoved.insertLast(EventHandler(events.onOutsideBorderSystemRemoved)); + return this; + } + + Events@ opAddAssign(IPlanetEvents& events) { + _onPlanetAdded.insertLast(EventHandler(events.onPlanetAdded)); + _onPlanetRemoved.insertLast(EventHandler(events.onPlanetRemoved)); + return this; + } + + Events@ opAddAssign(ITradeRouteEvents& events) { + _onTradeRouteNeeded.insertLast(EventHandler(events.onTradeRouteNeeded)); + return this; + } + + Events@ opAddAssign(IOrbitalRequestEvents& events) { + _onOrbitalRequested.insertLast(EventHandler(events.onOrbitalRequested)); + return this; + } + + //Event notifications + + private void raiseEvent(array& subscribed, ref@ sender, EventArgs& args) { + for (uint i = 0, cnt = subscribed.length; i < cnt; ++i) + subscribed[i](sender, args); + } + + void notifyOwnedSystemAdded(ref@ sender, EventArgs& args) { + raiseEvent(_onOwnedSystemAdded, sender, args); + } + + void notifyOwnedSystemRemoved(ref@ sender, EventArgs& args) { + raiseEvent(_onOwnedSystemRemoved, sender, args); + } + + void notifyBorderSystemAdded(ref@ sender, EventArgs& args) { + raiseEvent(_onBorderSystemAdded, sender, args); + } + + void notifyBorderSystemRemoved(ref@ sender, EventArgs& args) { + raiseEvent(_onBorderSystemRemoved, sender, args); + } + + void notifyOutsideBorderSystemAdded(ref@ sender, EventArgs& args) { + raiseEvent(_onOutsideBorderSystemAdded, sender, args); + } + + void notifyOutsideBorderSystemRemoved(ref@ sender, EventArgs& args) { + raiseEvent(_onOutsideBorderSystemRemoved, sender, args); + } + + void notifyPlanetAdded(ref@ sender, EventArgs& args) { + raiseEvent(_onPlanetAdded, sender, args); + } + + void notifyPlanetRemoved(ref@ sender, EventArgs& args) { + raiseEvent(_onPlanetRemoved, sender, args); + } + + void notifyTradeRouteNeeded(ref@ sender, EventArgs& args) { + raiseEvent(_onTradeRouteNeeded, sender, args); + } + + void notifyOrbitalRequested(ref@ sender, EventArgs& args) { + raiseEvent(_onOrbitalRequested, sender, args); + } + + void save(SaveFile& file) { + } + + void load(SaveFile& file) { + } +}; + +AIComponent@ createEvents() { + return Events(); +} ADDED scripts/server/empire_ai/weasel/Fleets.as Index: scripts/server/empire_ai/weasel/Fleets.as ================================================================== --- scripts/server/empire_ai/weasel/Fleets.as +++ scripts/server/empire_ai/weasel/Fleets.as @@ -0,0 +1,631 @@ +// Fleets +// ------ +// Manages data about fleets and missions, as well as making sure fleets +// return to their station after a mission. +// + +import empire_ai.weasel.WeaselAI; + +import empire_ai.weasel.Systems; +import empire_ai.weasel.Designs; +import empire_ai.weasel.Movement; + +enum FleetClass { + FC_Scout, + FC_Combat, + FC_Slipstream, + FC_Mothership, + FC_Defense, + + FC_ALL +}; + +enum MissionPriority { + MiP_Background, + MiP_Normal, + MiP_High, + MiP_Critical, +} + +class Mission { + int id = -1; + bool completed = false; + bool canceled = false; + uint priority = MiP_Normal; + + void _save(Fleets& fleets, SaveFile& file) { + file << completed; + file << canceled; + file << priority; + save(fleets, file); + } + + void _load(Fleets& fleets, SaveFile& file) { + file >> completed; + file >> canceled; + file >> priority; + load(fleets, file); + } + + void save(Fleets& fleets, SaveFile& file) { + } + + void load(Fleets& fleets, SaveFile& file) { + } + + bool get_isActive() { + return true; + } + + double getPerformWeight(AI& ai, FleetAI& fleet) { + return 1.0; + } + + void start(AI& ai, FleetAI& fleet) { + } + + void cancel(AI& ai, FleetAI& fleet) { + } + + void tick(AI& ai, FleetAI& fleet, double time) { + } +}; + +final class FleetAI { + uint fleetClass; + Object@ obj; + Mission@ mission; + + Region@ stationed; + bool stationedFactory = true; + + double filled = 0.0; + double idleSince = 0.0; + double fillStaticSince = 0.0; + + void save(Fleets& fleets, SaveFile& file) { + file << fleetClass; + file << stationed; + file << filled; + file << idleSince; + file << fillStaticSince; + file << stationedFactory; + + fleets.saveMission(file, mission); + } + + void load(Fleets& fleets, SaveFile& file) { + file >> fleetClass; + file >> stationed; + file >> filled; + file >> idleSince; + file >> fillStaticSince; + file >> stationedFactory; + + @mission = fleets.loadMission(file); + } + + bool get_isHome() { + if(stationed is null) + return true; + return obj.region is stationed; + } + + bool get_busy() { + return mission !is null; + } + + double get_strength() { + return obj.getFleetStrength(); + } + + double get_supplies() { + Ship@ ship = cast(obj); + if(ship is null) + return 1.0; + double maxSupply = ship.MaxSupply; + if(maxSupply <= 0) + return 1.0; + return ship.Supply / maxSupply; + } + + double get_remainingSupplies() { + Ship@ ship = cast(obj); + if(ship is null) + return 0.0; + return ship.Supply; + } + + double get_radius() { + return obj.getFormationRadius(); + } + + double get_fleetHealth() { + return obj.getFleetStrength() / obj.getFleetMaxStrength(); + } + + double get_flagshipHealth() { + Ship@ ship = cast(obj); + if(ship is null) + return 1.0; + return ship.blueprint.currentHP / ship.blueprint.design.totalHP; + } + + bool get_actionableState() { + if(isHome && obj.hasOrderedSupports && stationedFactory) + return false; + if(supplies < 0.75) + return false; + if(filled < 0.5) + return false; + if(filled < 1.0 && gameTime < fillStaticSince + 90.0) + return false; + return true; + } + + bool get_readyForAction() { + if(mission !is null) + return false; + if(isHome && obj.hasOrderedSupports && stationedFactory) + return false; + if(supplies < 0.75) + return false; + if(filled < 0.5) + return false; + if(filled < 1.0 && gameTime < fillStaticSince + 90.0) + return false; + if(obj.isMoving) { + if(obj.velocity.length / obj.maxAcceleration > 16.0) + return false; + } + //DOF - Do not send badly damaged flagships + Ship@ flagship = cast(obj); + auto@ bp = flagship.blueprint; + if(bp.currentHP / bp.design.totalHP < 0.75) { + return false; + } + return true; + } + + bool tick(AI& ai, Fleets& fleets, double time) { + //Make sure we still exist + if(!obj.valid || obj.owner !is ai.empire) { + if(mission !is null) { + mission.canceled = true; + @mission = null; + } + return false; + } + + //Record data + int supUsed = obj.SupplyUsed; + int supCap = obj.SupplyCapacity; + int supGhost = obj.SupplyGhost; + int supOrdered = obj.SupplyOrdered; + + double newFill = 1.0; + if(supCap > 0.0) + newFill = double(supUsed - supGhost - supOrdered) / double(supCap); + if(newFill != filled) { + fillStaticSince = gameTime; + filled = newFill; + } + + //Perform our mission + if(mission !is null) { + if(!mission.completed && !mission.canceled) + mission.tick(ai, this, time); + if(mission.completed || mission.canceled) { + @mission = null; + idleSince = gameTime; + } + } + + //Return to where we're stationed if we're not doing anything + if(mission is null && stationed !is null && fleetClass != FC_Scout) { + if(gameTime >= idleSince + ai.behavior.fleetIdleReturnStationedTime) { + if(obj.region !is stationed && !obj.hasOrders) { + if(fleets.log) + ai.print("Returning to station in "+stationed.name, obj); + fleets.movement.move(obj, stationed, spread=true); + } + } + } + return true; + } +}; + +class Fleets : AIComponent { + Systems@ systems; + Designs@ designs; + Movement@ movement; + + array fleets; + + int nextMissionId = 0; + double totalStrength = 0; + double totalMaxStrength = 0; + + void create() { + @systems = cast(ai.systems); + @designs = cast(ai.designs); + @movement = cast(ai.movement); + } + + void save(SaveFile& file) { + file << nextMissionId; + file << totalStrength; + file << totalMaxStrength; + + uint cnt = fleets.length; + file << cnt; + for(uint i = 0; i < cnt; ++i) { + saveAI(file, fleets[i]); + fleets[i].save(this, file); + } + } + + void load(SaveFile& file) { + file >> nextMissionId; + file >> totalStrength; + file >> totalMaxStrength; + + uint cnt = 0; + file >> cnt; + for(uint i = 0; i < cnt; ++i) { + FleetAI@ flAI = loadAI(file); + if(flAI !is null) + flAI.load(this, file); + else + FleetAI().load(this, file); + } + } + + void saveAI(SaveFile& file, FleetAI@ flAI) { + if(flAI is null) { + file.write0(); + return; + } + file.write1(); + file << flAI.obj; + } + + FleetAI@ loadAI(SaveFile& file) { + if(!file.readBit()) + return null; + + Object@ obj; + file >> obj; + + if(obj is null) + return null; + + for(uint i = 0, cnt = fleets.length; i < cnt; ++i) { + if(fleets[i].obj is obj) + return fleets[i]; + } + + FleetAI flAI; + @flAI.obj = obj; + fleets.insertLast(flAI); + return flAI; + } + + array savedMissions; + array loadedMissions; + void postSave(AI& ai) { + savedMissions.length = 0; + } + void postLoad(AI& ai) { + loadedMissions.length = 0; + } + + void saveMission(SaveFile& file, Mission@ mission) { + if(mission is null) { + file.write0(); + return; + } + + file.write1(); + file << mission.id; + if(mission.id == -1) { + storeMission(file, mission); + } + else { + bool found = false; + for(uint i = 0, cnt = savedMissions.length; i < cnt; ++i) { + if(savedMissions[i] is mission) { + found = true; + break; + } + } + + if(!found) { + storeMission(file, mission); + savedMissions.insertLast(mission); + } + } + } + + Mission@ loadMission(SaveFile& file) { + if(!file.readBit()) + return null; + + int id = 0; + file >> id; + if(id == -1) { + Mission@ miss = createMission(file); + miss.id = id; + return miss; + } + else { + for(uint i = 0, cnt = loadedMissions.length; i < cnt; ++i) { + if(loadedMissions[i].id == id) + return loadedMissions[i]; + } + + Mission@ miss = createMission(file); + miss.id = id; + loadedMissions.insertLast(miss); + return miss; + } + } + + void storeMission(SaveFile& file, Mission@ mission) { + auto@ cls = getClass(mission); + auto@ mod = cls.module; + + file << mod.name; + file << cls.name; + mission._save(this, file); + } + + Mission@ createMission(SaveFile& file) { + string modName; + string clsName; + + file >> modName; + file >> clsName; + + auto@ mod = getScriptModule(modName); + if(mod is null) { + error("ERROR: AI Load could not find module for mission "+modName+"::"+clsName); + return null; + } + + auto@ cls = mod.getClass(clsName); + if(cls is null) { + error("ERROR: AI Load could not find class for mission "+modName+"::"+clsName); + return null; + } + + auto@ miss = cast(cls.create()); + if(miss is null) { + error("ERROR: AI Load could not create class instance for mission "+modName+"::"+clsName); + return null; + } + + miss._load(this, file); + return miss; + } + + void checkForFleets() { + auto@ data = ai.empire.getFlagships(); + Object@ obj; + while(receive(data, obj)) { + if(obj !is null) + register(obj); + } + @data = ai.empire.getStations(); + while(receive(data, obj)) { + if(obj !is null) + register(obj); + } + } + + bool haveCombatReadyFleets() { + for(uint i = 0, cnt = fleets.length; i < cnt; ++i) { + auto@ flAI = fleets[i]; + if(flAI.fleetClass != FC_Combat) + continue; + if(!flAI.readyForAction) + continue; + return true; + } + return false; + } + + uint countCombatReadyFleets() { + uint count = 0; + for(uint i = 0, cnt = fleets.length; i < cnt; ++i) { + auto@ flAI = fleets[i]; + if(flAI.fleetClass != FC_Combat) + continue; + if(!flAI.readyForAction) + continue; + count += 1; + } + return count; + } + + bool allFleetsCombatReady() { + bool have = false; + for(uint i = 0, cnt = fleets.length; i < cnt; ++i) { + auto@ flAI = fleets[i]; + if(flAI.fleetClass != FC_Combat) + continue; + if(!flAI.readyForAction) + return false; + have = true; + } + return have; + } + + uint prevFleetCount = 0; + double checkTimer = 0; + void focusTick(double time) override { + //Check for any newly obtained fleets + uint curFleetCount = ai.empire.fleetCount; + checkTimer += time; + if(curFleetCount != prevFleetCount || checkTimer > 60.0) { + checkForFleets(); + prevFleetCount = curFleetCount; + checkTimer = 0; + } + + //Calculate our current strengths + totalStrength = 0; + totalMaxStrength = 0; + for(uint i = 0, cnt = fleets.length; i < cnt; ++i) { + totalStrength += sqrt(fleets[i].obj.getFleetStrength()); + totalMaxStrength += sqrt(fleets[i].obj.getFleetMaxStrength()); + } + totalStrength = sqr(totalStrength); + totalMaxStrength = sqr(totalMaxStrength); + } + + double getTotalStrength(uint checkClass, bool idleOnly = false, bool readyOnly = false) { + double str = 0.0; + for(uint i = 0, cnt = fleets.length; i < cnt; ++i) { + auto@ flAI = fleets[i]; + if((flAI.fleetClass == checkClass || checkClass == FC_ALL) + && (!idleOnly || flAI.mission is null) + && (!readyOnly || flAI.readyForAction)) + str += sqrt(fleets[i].obj.getFleetStrength()); + } + return str*str; + } + + void tick(double time) override { + for(uint i = 0, cnt = fleets.length; i < cnt; ++i) { + if(!fleets[i].tick(ai, this, time)) { + fleets.removeAt(i); + --i; --cnt; + continue; + } + + Region@ reg = fleets[i].obj.region; + if(reg !is null) + systems.focus(reg); + } + } + + MoveOrder@ returnToBase(FleetAI@ fleet, uint priority = MP_Normal) { + if(fleet.stationed !is null) + return movement.move(fleet.obj, fleet.stationed, priority, spread=true); + return null; + } + + FleetAI@ register(Object@ obj) { + FleetAI@ flAI = getAI(obj); + + if(flAI is null) { + @flAI = FleetAI(); + @flAI.obj = obj; + @flAI.stationed = obj.region; + obj.setHoldPosition(true); + + uint designClass = designs.classify(obj); + + if(designClass == DP_Scout) + flAI.fleetClass = FC_Scout; + else if(designClass == DP_Slipstream) + flAI.fleetClass = FC_Slipstream; + else if(designClass == DP_Mothership) + flAI.fleetClass = FC_Mothership; + else if(designClass == DP_Defense) + flAI.fleetClass = FC_Defense; + else + flAI.fleetClass = FC_Combat; + + fleets.insertLast(flAI); + } + + return flAI; + } + + void register(Mission@ mission) { + if(mission.id == -1) + mission.id = nextMissionId++; + } + + FleetAI@ getAI(Object@ obj) { + if(obj is null) + return null; + for(uint i = 0, cnt = fleets.length; i < cnt; ++i) { + if(fleets[i].obj is obj) + return fleets[i]; + } + return null; + } + + uint count(uint checkClass) { + uint amount = 0; + for(uint i = 0, cnt = fleets.length; i < cnt; ++i) { + auto@ flAI = fleets[i]; + if(flAI.fleetClass == checkClass || checkClass == FC_ALL) + amount += 1; + } + return amount; + } + + bool haveIdle(uint checkClass) { + for(uint i = 0, cnt = fleets.length; i < cnt; ++i) { + auto@ flAI = fleets[i]; + if((flAI.fleetClass == checkClass || checkClass == FC_ALL) && flAI.mission is null) + return true; + } + return false; + } + + double closestIdleTo(uint checkClass, const vec3d& position) { + double closest = INFINITY; + for(uint i = 0, cnt = fleets.length; i < cnt; ++i) { + auto@ flAI = fleets[i]; + if((flAI.fleetClass != checkClass && checkClass != FC_ALL) || flAI.mission !is null) + continue; + + double d = flAI.obj.position.distanceTo(position); + if(d < closest) + closest = d; + } + return closest; + } + + FleetAI@ performMission(Mission@ mission) { + FleetAI@ perform; + double bestWeight = 0.0; + for(uint i = 0, cnt = fleets.length; i < cnt; ++i) { + auto@ flAI = fleets[i]; + if(flAI.mission !is null) + continue; + double w = mission.getPerformWeight(ai, flAI); + if(w > bestWeight) { + bestWeight = w; + @perform = flAI; + } + } + + if(perform !is null) { + @perform.mission = mission; + register(mission); + mission.start(ai, perform); + } + return perform; + } + + FleetAI@ performMission(FleetAI@ fleet, Mission@ mission) { + if(fleet.mission !is null) { + fleet.mission.cancel(ai, fleet); + fleet.mission.canceled = true; + } + @fleet.mission = mission; + register(mission); + mission.start(ai, fleet); + return fleet; + } +}; + +AIComponent@ createFleets() { + return Fleets(); +} ADDED scripts/server/empire_ai/weasel/ImportData.as Index: scripts/server/empire_ai/weasel/ImportData.as ================================================================== --- scripts/server/empire_ai/weasel/ImportData.as +++ scripts/server/empire_ai/weasel/ImportData.as @@ -0,0 +1,330 @@ +import resources; +import tile_resources; +import saving; + +export ResourceSpecType; +export ResourceSpec; +export implementSpec; + +export ImportData; +export ExportData; + +enum ResourceSpecType { + RST_Specific, + RST_Level_Specific, + RST_Level_Minimum, + RST_Pressure_Type, + RST_Pressure_Level0, + RST_Class, +}; + +tidy final class ResourceSpec : Savable { + uint type = RST_Specific; + const ResourceType@ resource; + const ResourceClass@ cls; + uint level = 0; + uint pressureType = 0; + bool isLevelRequirement = false; + bool isForImport = true; + bool allowUniversal = true; + + void save(SaveFile& file) { + file << type; + if(resource !is null) { + file.write1(); + file.writeIdentifier(SI_Resource, resource.id); + } + else { + file.write0(); + } + if(cls !is null) { + file.write1(); + file << cls.ident; + } + else { + file.write0(); + } + file << level; + file << pressureType; + file << isLevelRequirement; + file << isForImport; + file << allowUniversal; + } + + void load(SaveFile& file) { + file >> type; + if(file.readBit()) + @resource = getResource(file.readIdentifier(SI_Resource)); + if(file.readBit()) { + string clsName; + file >> clsName; + @cls = getResourceClass(clsName); + } + file >> level; + file >> pressureType; + file >> isLevelRequirement; + file >> isForImport; + file >> allowUniversal; + } + + bool opEquals(const ResourceSpec& other) const { + if(type != other.type) + return false; + if(isLevelRequirement != other.isLevelRequirement) + return false; + switch(type) { + case RST_Specific: + return other.resource is resource; + case RST_Level_Specific: + case RST_Level_Minimum: + return other.level == level; + case RST_Pressure_Type: + case RST_Pressure_Level0: + return other.pressureType == pressureType; + case RST_Class: + return other.cls is cls; + } + return true; + } + + bool meets(const ResourceType@ check, Object@ fromObj = null, Object@ toObj = null) const { + if(check is null) + return false; + if(allowUniversal && isLevelRequirement) { + if(check.mode == RM_UniversalUnique || check.mode == RM_Universal) { + //HACK: The AI shouldn't use drugs for food and water + switch(type) { + case RST_Level_Specific: + case RST_Level_Minimum: + return level >= 2; + } + return false; + } + } + if(isForImport && !check.exportable && (fromObj is null || fromObj !is toObj)) + return false; + if(isLevelRequirement && check.mode == RM_NonRequirement) + return false; + switch(type) { + case RST_Specific: + return check is resource; + case RST_Level_Specific: + return check.level == level; + case RST_Level_Minimum: + return check.level >= level; + case RST_Pressure_Type: + return check.tilePressure[pressureType] >= max(check.totalPressure * 0.4, 1.0); + case RST_Pressure_Level0: + return check.level == 0 && check.tilePressure[pressureType] >= max(check.totalPressure * 0.4, 1.0); + case RST_Class: + return check.cls is cls; + } + return false; + } + + bool implements(const ResourceRequirement& req) const { + if(!isLevelRequirement) + return false; + switch(req.type) { + case RRT_Resource: + return this.type == RST_Specific && this.resource is req.resource; + case RRT_Class: + case RRT_Class_Types: + return this.type == RST_Class && this.cls is req.cls; + case RRT_Level: + case RRT_Level_Types: + return this.type == RST_Level_Specific && this.level == req.level; + } + return false; + } + + string dump() { + switch(type) { + case RST_Specific: + return resource.name; + case RST_Level_Specific: + return "Tier "+level; + case RST_Level_Minimum: + return "Tier "+level+"+"; + case RST_Pressure_Type: + return "Any "+getTileResourceIdent(pressureType); + case RST_Pressure_Level0: + return "Level 0 "+getTileResourceIdent(pressureType); + case RST_Class: + return "Of "+cls.ident; + } + return "??"; + } + + int get_resourceLevel() const { + switch(type) { + case RST_Specific: + return 0; + case RST_Level_Specific: + return level; + case RST_Level_Minimum: + return level; + case RST_Pressure_Type: + return 0; + case RST_Pressure_Level0: + return 0; + case RST_Class: + return 0; + } + return 0; + } + + int opCmp(const ResourceSpec@ other) const { + int level = this.resourceLevel; + int otherLevel = other.resourceLevel; + if(level > otherLevel) + return 1; + if(level < otherLevel) + return -1; + return 0; + } +}; + +ResourceSpec@ implementSpec(const ResourceRequirement& req) { + ResourceSpec spec; + spec.isLevelRequirement = true; + + switch(req.type) { + case RRT_Resource: + spec.type = RST_Specific; + @spec.resource = req.resource; + break; + case RRT_Class: + case RRT_Class_Types: + spec.type = RST_Class; + @spec.cls = req.cls; + break; + case RRT_Level: + case RRT_Level_Types: + spec.type = RST_Level_Specific; + spec.level = req.level; + break; + } + return spec; +} + +tidy final class ImportData : Savable { + int id = -1; + Object@ obj; + ResourceSpec@ spec; + const ResourceType@ resource; + Object@ fromObject; + int resourceId = -1; + bool beingMet = false; + bool forLevel = false; + bool cycled = false; + bool isColonizing = false; + bool claimedFor = false; + double idleSince = 0.0; + + void save(SaveFile& file) { + file << obj; + file << spec; + if(resource !is null) { + file.write1(); + file.writeIdentifier(SI_Resource, resource.id); + } + else { + file.write0(); + } + file << fromObject; + file << resourceId; + file << beingMet; + file << forLevel; + file << cycled; + file << isColonizing; + file << claimedFor; + file << idleSince; + } + + void load(SaveFile& file) { + file >> obj; + @spec = ResourceSpec(); + file >> spec; + if(file.readBit()) + @resource = getResource(file.readIdentifier(SI_Resource)); + file >> fromObject; + file >> resourceId; + file >> beingMet; + file >> forLevel; + file >> cycled; + file >> isColonizing; + file >> claimedFor; + file >> idleSince; + } + + void set(ExportData@ source) { + @fromObject = source.obj; + resourceId = source.resourceId; + @resource = source.resource; + } + + int opCmp(const ImportData@ other) const { + return spec.opCmp(other.spec); + } + + bool get_isOpen() const { + return !beingMet; + } +}; + +tidy final class ExportData : Savable { + int id = -1; + Object@ obj; + const ResourceType@ resource; + int resourceId = -1; + ImportData@ request; + Object@ developUse; + bool localOnly = false; + + bool get_usable() const { + if(obj is null) + return false; + if(resourceId == obj.primaryResourceId) + return obj.primaryResourceUsable; + else + return obj.getNativeResourceUsableByID(resourceId); + } + + bool get_isPrimary() const { + return resourceId == obj.primaryResourceId; + } + + bool isExportedTo(Object@ check) const { + if(check is obj) + return true; + if(resourceId == obj.primaryResourceId) + return obj.isPrimaryDestination(check); + else + return obj.getNativeResourceDestinationByID(obj.owner, resourceId) is check; + } + + void save(SaveFile& file) { + //Does not save the request link, this is done by Resources + file << obj; + if(resource !is null) { + file.write1(); + file.writeIdentifier(SI_Resource, resource.id); + } + else { + file.write0(); + } + file << resourceId; + file << developUse; + file << localOnly; + } + + void load(SaveFile& file) { + file >> obj; + if(file.readBit()) + @resource = getResource(file.readIdentifier(SI_Resource)); + file >> resourceId; + file >> developUse; + file >> localOnly; + } +}; ADDED scripts/server/empire_ai/weasel/Infrastructure.as Index: scripts/server/empire_ai/weasel/Infrastructure.as ================================================================== --- scripts/server/empire_ai/weasel/Infrastructure.as +++ scripts/server/empire_ai/weasel/Infrastructure.as @@ -0,0 +1,1334 @@ +// Infrastructure +// ------ +// Manages building basic structures in newly colonized or weakened systems +// to support the Military or Colonization components. +// +import empire_ai.weasel.WeaselAI; + +import empire_ai.weasel.Events; +import empire_ai.weasel.Colonization; +import empire_ai.weasel.Development; +import empire_ai.weasel.Construction; +import empire_ai.weasel.Budget; +import empire_ai.weasel.Orbitals; +import empire_ai.weasel.Systems; +import empire_ai.weasel.Planets; +import empire_ai.weasel.Resources; + +import ai.construction; +import ai.events; + +from ai.orbitals import RegisterForTradeUse; + +from statuses import getStatusID; +from traits import getTraitID; + +enum ResourcePreference { + RP_None, + RP_FoodWater, + RP_Level0, + RP_Level1, + RP_Level2, + RP_Level3, + RP_Scalable, +}; + +enum SystemArea { + SA_Core, + SA_Tradable, +}; + +enum SystemBuildAction { + BA_BuildOutpost, +}; + +enum PlanetBuildAction { + BA_BuildMoonBase, +}; + +enum SystemBuildLocation { + BL_InSystem, + BL_AtSystemEdge, + BL_AtBestPlanet, +}; + +enum FocusType { + FT_None, + FT_Outpost, +} + +int moonBaseStatusId = -1; + +final class OwnedSystemEvents : IOwnedSystemEvents { + Infrastructure@ infrastructure; + + OwnedSystemEvents(Infrastructure& infrastructure) { + @this.infrastructure = infrastructure; + } + + void onOwnedSystemAdded(ref& sender, EventArgs& args) { + SystemAI@ ai = cast(sender); + if (ai !is null) + infrastructure.registerOwnedSystemAdded(ai); + } + + void onOwnedSystemRemoved(ref& sender, EventArgs& args) { + SystemAI@ ai = cast(sender); + if (ai !is null) + infrastructure.registerOwnedSystemRemoved(ai); + } +}; + +final class OutsideBorderSystemEvents : IOutsideBorderSystemEvents { + Infrastructure@ infrastructure; + + OutsideBorderSystemEvents(Infrastructure& infrastructure) { + @this.infrastructure = infrastructure; + } + + void onOutsideBorderSystemAdded(ref& sender, EventArgs& args) { + SystemAI@ ai = cast(sender); + if (ai !is null) + infrastructure.registerOutsideBorderSystemAdded(ai); + } + + void onOutsideBorderSystemRemoved(ref& sender, EventArgs& args) { + SystemAI@ ai = cast(sender); + if (ai !is null) + infrastructure.registerOutsideBorderSystemRemoved(ai); + } +}; + +final class PlanetEvents : IPlanetEvents { + Infrastructure@ infrastructure; + + PlanetEvents(Infrastructure& infrastructure) { + @this.infrastructure = infrastructure; + } + + void onPlanetAdded(ref& sender, EventArgs& args) { + PlanetAI@ ai = cast(sender); + if (ai !is null) + infrastructure.registerPlanetAdded(ai); + } + + void onPlanetRemoved(ref& sender, EventArgs& args) { + PlanetAI@ ai = cast(sender); + if (ai !is null) + infrastructure.registerPlanetRemoved(ai); + } +}; + +final class TradeRouteEvents : ITradeRouteEvents { + Infrastructure@ infrastructure; + + TradeRouteEvents(Infrastructure& infrastructure) { + @this.infrastructure = infrastructure; + } + + void onTradeRouteNeeded(ref& sender, EventArgs& args) { + TradeRouteNeededEventArgs@ specs = cast(args); + if (specs !is null) + infrastructure.establishTradeRoute(specs.territoryA, specs.territoryB); + } +} + +final class OrbitalRequestEvents : IOrbitalRequestEvents { + Infrastructure@ infrastructure; + + OrbitalRequestEvents(Infrastructure& infrastructure) { + @this.infrastructure = infrastructure; + } + + void onOrbitalRequested(ref& sender, EventArgs& args) { + OrbitalRequestedEventArgs@ specs = cast(args); + if (specs !is null) + infrastructure.requestOrbital(specs.region, specs.module, specs.priority, specs.expires, specs.moneyType); + } +} + +final class SystemOrder { + private IConstruction@ _construction; + + double expires = INFINITY; + + SystemOrder() {} + + SystemOrder(IConstruction@ construction) { + @_construction = (construction); + } + + bool get_isValid() const { return _construction !is null; } + + bool get_isInProgress() const { return _construction.started; } + + bool get_isComplete() const { return _construction.completed; } + + IConstruction@ get_info() const { return _construction; } + + void save(Infrastructure& infrastructure, SaveFile& file) { + file << _construction.id; + file << expires; + } + + void load(Infrastructure& infrastructure, SaveFile& file) { + int id = - 1; + file >> id; + if (id != -1) { + for (uint i = 0, cnt = infrastructure.construction.allocations.length; i < cnt; ++i) { + if (infrastructure.construction.allocations[i].id == id) { + @_construction = infrastructure.construction.allocations[i]; + } + } + } + file >> expires; + } +}; + +final class PlanetOrder { + private IConstruction@ _construction; + + double expires = INFINITY; + + PlanetOrder() {} + + PlanetOrder(IConstruction@ construction) { + @_construction = construction; + } + + bool get_isValid() const { return _construction !is null; } + + bool get_isInProgress() const { return _construction.started; } + + bool get_isComplete() const { return _construction.completed; } + + IConstruction@ get_info() const { return _construction; } + + void save(Infrastructure& infrastructure, SaveFile& file) { + file << _construction.id; + file << expires; + } + + void load(Infrastructure& infrastructure, SaveFile& file) { + int id = - 1; + file >> id; + if (id != -1) { + for (uint i = 0, cnt = infrastructure.planets.constructionRequests.length; i < cnt; ++i) { + if (infrastructure.planets.constructionRequests[i].id == id) { + @_construction = infrastructure.planets.constructionRequests[i]; + } + } + } + file >> expires; + } +}; + +abstract class NextAction { + double priority = 1.0; + bool force = false; + bool critical = false; +}; + +final class SystemAction : NextAction { + private SystemCheck@ _sys; + private SystemBuildAction _action; + private SystemBuildLocation _loc; + + SystemAction(SystemCheck& sys, SystemBuildAction action, SystemBuildLocation loc) { + @_sys = sys; + _action = action; + _loc = loc; + } + + SystemCheck@ get_sys() const { return _sys; } + SystemBuildAction get_action() const { return _action; } + SystemBuildLocation get_loc() const { return _loc; } +}; + +final class PlanetAction : NextAction { + private PlanetCheck@ _pl; + private PlanetBuildAction _action; + + PlanetAction(PlanetCheck& pl, PlanetBuildAction action) { + @_pl = pl; + _action = action; + } + + PlanetCheck@ get_pl() const { return _pl; } + PlanetBuildAction get_action() const { return _action; } +}; + +abstract class Check { + protected double _checkInTime = 0.0; + + Check() { + _checkInTime = gameTime; + } + + double get_checkInTime() const { return _checkInTime; } +} + +namespace SystemCheck { + array allOrders; +} + +final class SystemCheck : Check { + SystemAI@ ai; + + array orders; + + private double _weight = 0.0; + private bool _isUnderAttack = false; + + SystemCheck() {} + + SystemCheck(Infrastructure& infrastructure, SystemAI& ai) { + super(); + @this.ai = ai; + } + + double get_weight() const { return _weight; } + bool get_isUnderAttack() const { return _isUnderAttack; } + bool get_isBuilding() const { return orders.length > 0; } + + void save(Infrastructure& infrastructure, SaveFile& file) { + infrastructure.systems.saveAI(file, ai); + + uint cnt = orders.length; + file << cnt; + for(uint i = 0; i < cnt; ++i) + orders[i].save(infrastructure, file); + + file << _checkInTime; + file << _weight; + file << _isUnderAttack; + } + + void load(Infrastructure& infrastructure, SaveFile& file) { + @ai = infrastructure.systems.loadAI(file); + + uint cnt = 0; + file >> cnt; + for(uint i = 0; i < cnt; ++i) { + auto@ order = SystemOrder(); + order.load(infrastructure, file); + if (order.isValid) + addOrder(order); + } + file >> _checkInTime; + file >> _weight; + file >> _isUnderAttack; + } + + void tick(AI& ai, Infrastructure& infrastructure, double time) { + OrbitalAI@ orbital; + //Update hostile status + _isUnderAttack = this.ai.obj.ContestedMask & ai.mask != 0; + + //Cancel all orders if attacked + /*if (isUnderAttack && isBuilding) { + for (uint i = 0, cnt = orders.length; i < cnt; ++i) { + auto@ order = orders[i]; + //SoI - TODO: Cancel not fully implemented, see Construction.as + infrastructure.construction.cancel(order.info); + removeOrder(order); + --i; --cnt; + } + }*/ + + if (isBuilding) { + for (uint i = 0, cnt = orders.length; i < cnt; ++i) { + auto@ order = orders[i]; + if (!order.isValid) { + removeOrder(order); + --i; --cnt; + } + else if (order.isComplete) { + if (infrastructure.log) + ai.print("order complete"); + removeOrder(order); + --i; --cnt; + } + else if (!order.isInProgress && order.expires < gameTime) { + if (infrastructure.log) + ai.print("order expired, gameTime = " + gameTime); + removeOrder(order); + --i; --cnt; + } + } + } + } + + void focusTick(AI& ai, Infrastructure& infrastructure, double time) { + } + + double check(AI& ai) { + _weight = 0.0; + //Systems under attack are bottom priority for now + if (isUnderAttack) + return weight; + //Hostile systems are bottom priority until cleared + if (this.ai.seenPresent & ai.enemyMask != 0) + return weight; + //Start weighting + double sysWeight = 1.0; + //Oldest systems come first + sysWeight /= (checkInTime + 60.0) / 60.0; + //The home system is a priority + if (this.ai.obj is ai.empire.HomeSystem) + sysWeight *= 2.0; + + _weight = 1.0 * sysWeight; + return weight; + } + + SystemOrder@ buildInSystem(Infrastructure& infrastructure, const OrbitalModule@ module, double priority = 1.0, bool force = false, double delay = 600.0, uint moneyType = BT_Infrastructure) { + vec3d pos = ai.obj.position; + vec2d offset = random2d(ai.obj.radius * 0.4, ai.obj.radius * 0.7); + pos.x += offset.x; + pos.z += offset.y; + + BuildOrbital@ orbital = infrastructure.construction.buildOrbital(module, pos, priority, force, moneyType); + auto@ order = SystemOrder(orbital); + order.expires = gameTime + delay; + addOrder(order); + + return order; + } + + SystemOrder@ buildAtSystemEdge(Infrastructure& infrastructure, const OrbitalModule@ module, double priority = 1.0, bool force = false, double delay = 600.0, uint moneyType = BT_Infrastructure) { + vec3d pos = ai.obj.position; + vec2d offset = random2d(ai.obj.radius * 0.8, ai.obj.radius * 0.9); + pos.x += offset.x; + pos.z += offset.y; + + BuildOrbital@ orbital = infrastructure.construction.buildOrbital(module, pos, priority, force, moneyType); + auto@ order = SystemOrder(orbital); + order.expires = gameTime + delay; + addOrder(order); + + return order; + } + + SystemOrder@ buildAtPlanet(Infrastructure& infrastructure, Planet& planet, const OrbitalModule@ module, double priority = 1.0, bool force = false, double delay = 600.0, uint moneyType = BT_Infrastructure) { + BuildOrbital@ orbital = infrastructure.construction.buildLocalOrbital(module, planet, priority, force, moneyType); + auto@ order = SystemOrder(orbital); + order.expires = gameTime + delay; + addOrder(order); + + return order; + } + + void addOrder(SystemOrder@ order) { + orders.insertLast(order); + SystemCheck::allOrders.insertLast(order); + } + + void removeOrder(SystemOrder@ order) { + orders.remove(order); + SystemCheck::allOrders.remove(order); + @order = null; + } +}; + +namespace PlanetCheck { + array allOrders; +} + +final class PlanetCheck : Check { + PlanetAI@ ai; + + array orders; + + private double _weight = 0.0; + private bool _isSystemUnderAttack = false; + + PlanetCheck() {} + + PlanetCheck(Infrastructure& infrastructure, PlanetAI& ai) { + super(); + @this.ai = ai; + } + + double get_weight() const { return _weight; } + bool get_isSystemUnderAttack() const { return _isSystemUnderAttack; } + bool get_isBuilding() const { return orders.length > 0; } + + void save(Infrastructure& infrastructure, SaveFile& file) { + infrastructure.planets.saveAI(file, ai); + + uint cnt = orders.length; + file << cnt; + for(uint i = 0; i < cnt; ++i) + orders[i].save(infrastructure, file); + + file << _checkInTime; + file << _weight; + file << _isSystemUnderAttack; + } + + void load(Infrastructure& infrastructure, SaveFile& file) { + @ai = infrastructure.planets.loadAI(file); + + uint cnt = 0; + file >> cnt; + for(uint i = 0; i < cnt; ++i) { + auto@ order = PlanetOrder(); + order.load(infrastructure, file); + if (order.isValid) + addOrder(order); + } + file >> _checkInTime; + file >> _weight; + file >> _isSystemUnderAttack; + } + + void tick(AI& ai, Infrastructure& infrastructure, double time) { + auto@ sysAI = infrastructure.systems.getAI(this.ai.obj.region); + if (sysAI !is null) + _isSystemUnderAttack = sysAI.obj.ContestedMask & ai.mask != 0; + + if (isBuilding) { + for (uint i = 0, cnt = orders.length; i < cnt; ++i) { + auto@ order = orders[i]; + if (!order.isValid) { + removeOrder(order); + --i; --cnt; + } + else if (order.isComplete) { + if (infrastructure.log) + ai.print("planet order complete"); + removeOrder(order); + --i; --cnt; + } + else if (!order.isInProgress && order.expires < gameTime) { + if (infrastructure.log) + ai.print("planet order expired, gameTime = " + gameTime); + removeOrder(order); + --i; --cnt; + } + } + } + } + + void focusTick(AI& ai, Infrastructure& infrastructure, double time) { + } + + double check(AI& ai) { + _weight = 0.0; + //Planets in systems under attack are bottom priority for now + if (isSystemUnderAttack) + return _weight; + //Start weighting + double plWeight = 1.0; + //Oldest planets come first + plWeight /= (checkInTime + 60.0) / 60.0; + //The homeworld is a priority + if (this.ai.obj is ai.empire.Homeworld) + plWeight *= 2.0; + + _weight = 1.0 * plWeight; + return _weight; + } + + PlanetOrder@ build(Infrastructure& infrastructure, const ConstructionType@ consType, double priority = 1.0, bool force = false, bool critical = false, double delay = 600.0, uint moneyType = BT_Infrastructure) { + ConstructionRequest@ request = infrastructure.planets.requestConstruction(ai, ai.obj, consType, priority, gameTime + delay, moneyType); + auto@ order = PlanetOrder(request); + order.expires = gameTime + delay; + addOrder(order); + + return order; + } + + void addOrder(PlanetOrder@ order) { + orders.insertLast(order); + PlanetCheck::allOrders.insertLast(order); + } + + void removeOrder(PlanetOrder@ order) { + orders.remove(order); + PlanetCheck::allOrders.remove(order); + @order = null; + } +}; + +final class TradeRoute { + private Territory@ _territoryA; + private Territory@ _territoryB; + private Region@ _endpointA; + private Region@ _endpointB; + private SystemOrder@ _orderA; + private SystemOrder@ _orderB; + private bool _isEstablishing; + private bool _isWaitingForLabor; + private double _delay; + private double _sleep; + + TradeRoute() {} + + TradeRoute(Territory& territoryA, Territory& territoryB) { + @_territoryA = territoryA; + @_territoryB = territoryB; + _isEstablishing = false; + _isWaitingForLabor = false; + _delay = 0.0; + _sleep = 0.0; + } + + Territory@ get_territoryA() const { return _territoryA; } + Territory@ get_territoryB() const { return _territoryB; } + Region@ get_endpointA() const { return _endpointA; } + Region@ get_endpointB() const { return _endpointB; } + SystemOrder@ get_orderA() const { return _orderA; } + SystemOrder@ get_orderB() const { return _orderB; } + bool get_isEstablishing() const { return _isEstablishing; } + bool get_isWaitingForLabor() const { return _isWaitingForLabor; } + + void save(Infrastructure& infrastructure, SaveFile& file) { + } + + void load(Infrastructure& infrastructure, SaveFile& file) { + } + + void tick(AI& ai, Infrastructure& infrastructure, double time) { + if (_delay > 0.0 && _delay < gameTime) { + _isWaitingForLabor = false; + _delay = 0.0; + } + } + + void focusTick(AI& ai, Infrastructure& infrastructure, double time) { + } + + bool canEstablish(Infrastructure& infrastructure, bool&out buildAtA, bool&out canBuildAtA, bool&out buildAtB, bool&out canBuildAtB) { + //We're still sleeping + if (_sleep > gameTime) + return false; + //At least one building order is still pending + if (orderA !is null || orderB !is null) + return false; + + buildAtA = true; + buildAtB = true; + canBuildAtA = false; + canBuildAtB = false; + for (uint i = 0, cnt = infrastructure.checkedPlanets.length; i < cnt; ++i) { + Planet@ pl = infrastructure.checkedPlanets[i].ai.obj; + if (pl.region !is null) { + Territory@ t = pl.region.getTerritory(infrastructure.ai.empire); + if (t is territoryA) { + //Is there a global trade node here already + if (pl.region.GateMask & ~pl.owner.mask != 0) { + buildAtA = false; + @_endpointA = pl.region; + } + if (!canBuildAtA) { + //Is there a labor source in this territory + if (pl.laborIncome > 0 && pl.canBuildOrbitals) + canBuildAtA = true; + } + } + else if (t is territoryB) { + //Is there a global trade node here already + if (pl.region.GateMask & ~pl.owner.mask != 0) { + buildAtB = false; + @_endpointB = pl.region; + } + if (!canBuildAtB) { + //Is there a labor source in this territory + if (pl.laborIncome > 0 && pl.canBuildOrbitals) + canBuildAtB = true; + } + } + if (!buildAtA && !buildAtB) { + //Should not normally happen, except if trade if somehow disrupted despite global trade nodes + return false; + } + if (canBuildAtA && canBuildAtB) { + _isWaitingForLabor = false; + return true; + } + } + } + //These checks are expensive and don't need to be run frequently, so let's sleep for some time + _sleep = gameTime + 10.0; + return false; + } + + void establish(Infrastructure& infrastructure, Region@ regionA, Region@ regionB) { + SystemOrder@ orderA = null; + SystemOrder@ orderB = null; + if (regionA !is null) { + @orderA = infrastructure.requestOrbital(regionA, infrastructure.ai.defs.TradeStation); + @_endpointA = regionA; + } + if (regionB !is null) { + @orderB = infrastructure.requestOrbital(regionB, infrastructure.ai.defs.TradeStation); + @_endpointB = regionB; + } + if (orderA is null || orderB is null) { + infrastructure.ai.print("ERROR: could not establish trade route between " + regionA.name + " and " + regionB.name); + return; + } + @_orderA = orderA; + @_orderB = orderB; + _isEstablishing = true; + } + + void waitForLabor(double expires) { + _isWaitingForLabor = true; + _delay = gameTime + expires; + } +} + +final class Infrastructure : AIComponent { + const ResourceClass@ foodClass, waterClass, scalableClass; + + //Current focus + private uint _focus = FT_None; + + Events@ events; + Colonization@ colonization; + Development@ development; + Construction@ construction; + Orbitals@ orbitals; + Planets@ planets; + Systems@ systems; + Budget@ budget; + Resources@ resources; + + array checkedOwnedSystems; //Includes border systems + array checkedOutsideSystems; + array checkedPlanets; + + array pendingRoutes; + + SystemCheck@ homeSystem; + NextAction@ nextAction; + + //Unlock tracking + bool canBuildGate = false; + bool canBuildMoonBase = true; + + void create() { + @events = cast(ai.events); + @colonization = cast(ai.colonization); + @development = cast(ai.development); + @construction = cast(ai.construction); + @orbitals = cast(ai.orbitals); + @planets = cast(ai.planets); + @systems = cast(ai.systems); + @budget = cast(ai.budget); + @resources = cast(ai.resources); + + //Cache expensive lookups + @foodClass = getResourceClass("Food"); + @waterClass = getResourceClass("WaterType"); + @scalableClass = getResourceClass("Scalable"); + moonBaseStatusId = getStatusID("MoonBase"); + + events += OwnedSystemEvents(this); + events += OutsideBorderSystemEvents(this); + events += PlanetEvents(this); + events += TradeRouteEvents(this); + + if (ai.empire.hasTrait(getTraitID("Gate"))) + canBuildGate = true; + if (ai.empire.hasTrait(getTraitID("StarChildren"))) + canBuildMoonBase = false; + } + + void save(SaveFile& file) { + file << _focus; + uint cnt = checkedOwnedSystems.length; + file << cnt; + for(uint i = 0; i < cnt; ++i) + checkedOwnedSystems[i].save(this, file); + cnt = checkedOutsideSystems.length; + file << cnt; + for(uint i = 0; i < cnt; ++i) + checkedOutsideSystems[i].save(this, file); + cnt = checkedPlanets.length; + file << cnt; + for(uint i = 0; i < cnt; ++i) + checkedPlanets[i].save(this, file); + } + + void load(SaveFile& file) { + file >> _focus; + uint cnt = 0; + file >> cnt; + for(uint i = 0; i < cnt; ++i) { + SystemCheck@ sys = SystemCheck(); + sys.load(this, file); + checkedOwnedSystems.insertLast(sys); + } + cnt = 0; + file >> cnt; + for(uint i = 0; i < cnt; ++i) { + SystemCheck@ sys = SystemCheck(); + sys.load(this, file); + checkedOutsideSystems.insertLast(sys); + } + cnt = 0; + file >> cnt; + for(uint i = 0; i < cnt; ++i) { + PlanetCheck@ pl = PlanetCheck(); + pl.load(this, file); + checkedPlanets.insertLast(pl); + } + } + + void start() { + } + + void turn() { + if(log) { + ai.print("=============="); + ai.print("Current owned systems checked: " + checkedOwnedSystems.length); + for (uint i = 0, cnt = checkedOwnedSystems.length; i < cnt; ++i) + ai.print(checkedOwnedSystems[i].ai.obj.name); + ai.print("=============="); + ai.print("Current outside border systems checked: " + checkedOutsideSystems.length); + for (uint i = 0, cnt = checkedOutsideSystems.length; i < cnt; ++i) + ai.print(checkedOutsideSystems[i].ai.obj.name); + ai.print("=============="); + ai.print("Current owned planets checked: " + checkedPlanets.length); + for (uint i = 0, cnt = checkedPlanets.length; i < cnt; ++i) + ai.print(checkedPlanets[i].ai.obj.name); + ai.print("=============="); + } + + //Reset any focus + _focus = FT_None; + //If colonization is somehow blocked, force territory expansion by focusing on building outposts + if (colonization.needsMoreTerritory){ + if (budget.canFocus()) { + budget.focus(BT_Infrastructure); + _focus = FT_Outpost; + } + } + } + + void tick(double time) override { + SystemCheck@ sys; + PlanetCheck@ pl; + TradeRoute@ route; + //Perform routine duties + for (uint i = 0, cnt = checkedOwnedSystems.length; i < cnt; ++i) { + @sys = checkedOwnedSystems[i]; + sys.tick(ai, this, time); + } + for (uint i = 0, cnt = checkedOutsideSystems.length; i < cnt; ++i) { + @sys = checkedOutsideSystems[i]; + sys.tick(ai, this, time); + } + for (uint i = 0, cnt = checkedPlanets.length; i < cnt; ++i) { + @pl = checkedPlanets[i]; + pl.tick(ai, this, time); + } + for (uint i = 0, cnt = pendingRoutes.length; i < cnt; ++i) { + @route = pendingRoutes[i]; + route.tick(ai, this, time); + } + } + + void focusTick(double time) override { + SystemCheck@ sys; + PlanetCheck@ pl; + SystemBuildLocation loc; + + bool critical = false; + double w; + double bestWeight = 0.0; + + if(ai.behavior.forbidConstruction) return; + + //Check if owned systems need anything + for (uint i = 0, cnt = checkedOwnedSystems.length; i < cnt; ++i) { + @sys = checkedOwnedSystems[i]; + //Only consider anything if no critical action is underway + if (!critical) { + //Evaluate current weight + w = sys.check(ai); + if (w > bestWeight) { + if (_focus == FT_None || _focus == FT_Outpost) { + //Check if an outpost is needed + if (shouldHaveOutpost(sys, SA_Core, loc)) { + @nextAction = SystemAction(sys, BA_BuildOutpost, loc); + bestWeight = w; + if (log) + ai.print("outpost considered for owned system with weight: " + w, sys.ai.obj); + } + } + } + } + //Perform routine duties + sys.focusTick(ai, this, time); + } + //Check if systems in tradable area need anything + for (uint i = 0, cnt = checkedOutsideSystems.length; i < cnt; ++i) { + @sys = checkedOutsideSystems[i]; + //Skip unexplored systems + if (sys.ai.explored) { + //Only consider anything if no critical action is underway + if (!critical) { + //Evaluate current weight + w = sys.check(ai); + if (w > bestWeight) { + if (_focus == FT_None || _focus == FT_Outpost) { + //Check if an outpost is needed + if (shouldHaveOutpost(sys, SA_Tradable, loc)) { + @nextAction = SystemAction(sys, BA_BuildOutpost, loc); + bestWeight = w; + if (log) + ai.print("outpost considered for outside system with weight: " + w, sys.ai.obj); + } + } + } + } + } + //Perform routine duties + sys.focusTick(ai, this, time); + } + //Check if owned planets need anything + for (uint i = 0, cnt = checkedPlanets.length; i < cnt; ++i) { + @pl = checkedPlanets[i]; + //Only consider anything if no critical action is underway + if (!critical) { + //Planets are their own 'factory' and can only build one construction at a time + if (!pl.isBuilding) { + //Evaluate current weight + w = pl.check(ai); + if (w > bestWeight) { + //Check if a moon base is needed + if (canBuildMoonBase && shouldHaveMoonBase(pl)) { + @nextAction = PlanetAction(pl, BA_BuildMoonBase); + bestWeight = w; + if (log) + ai.print("moon base considered with weight: " + w, pl.ai.obj); + } + } + } + } + //Perform routine duties + pl.focusTick(ai, this, time); + } + //Execute our next action if there is one + if (nextAction !is null) { + Object@ obj; + auto@ next = cast(nextAction); + if (next !is null) + { + @sys = next.sys; + switch (next.action) { + case BA_BuildOutpost: + switch (next.loc) { + case BL_InSystem: + sys.buildInSystem(this, ai.defs.TradeOutpost, next.priority, next.force); + break; + case BL_AtSystemEdge: + sys.buildAtSystemEdge(this, ai.defs.TradeOutpost, next.priority, next.force); + break; + case BL_AtBestPlanet: + @obj = getBestPlanet(sys); + if (obj !is null) { + sys.buildAtPlanet(this, cast(obj), ai.defs.TradeOutpost, next.priority, next.force); + } + break; + default: + ai.print("ERROR: undefined infrastructure building location for outpost"); + } + if (log) + ai.print("outpost ordered", sys.ai.obj); + break; + default: + ai.print("ERROR: undefined infrastructure building action for system"); + } + } + else { + auto@ next = cast(nextAction); + if (next !is null) { + @pl = next.pl; + switch (next.action) { + case BA_BuildMoonBase: + pl.build(this, ai.defs.MoonBase, next.priority, next.force, next.critical); + if (log) + ai.print("moon base ordered", pl.ai.obj); + break; + default: + ai.print("ERROR: undefined infrastructure building action for planet"); + } + } + } + + @nextAction = null; + } + + //Manage any pending trading routes + TradeRoute@ route; + for (uint i = 0, cnt = pendingRoutes.length; i < cnt; ++i) { + @route = pendingRoutes[i]; + + if (route.territoryA is null || route.territoryB is null) { + pendingRoutes.remove(route); + --i; --cnt; + if (log) + ai.print("invalid territory for pending route, route canceled"); + } + + bool buildAtA = true; + bool canBuildAtA = false; + bool buildAtB = true; + bool canBuildAtB = false; + + if (route.canEstablish(this, buildAtA, canBuildAtA, buildAtB, canBuildAtB)) { + Region@ regionA = null; + Region@ regionB = null; + if (buildAtA) + @regionA = getRouteEndpoint(route.territoryA); + if (buildAtB) + @regionB = getRouteEndpoint(route.territoryB); + if (regionA !is null && regionB !is null) { + route.establish(this, regionA, regionB); + if (log) + ai.print("trade route establishing between " + regionA.name + " and " + regionB.name); + } + } + else if (!route.isEstablishing && !route.isWaitingForLabor) { + Region@ regionA = null; + Region@ regionB = null; + double expires = 0.0; + if (!canBuildAtA) + @regionA = getLaborAt(route.territoryA, expires); + if (!canBuildAtB) + @regionB = getLaborAt(route.territoryB, expires); + route.waitForLabor(expires); + if (log) { + string location = ""; + if (!canBuildAtA && regionA !is null) + location += " " + regionA.name; + if (!canBuildAtB && regionB !is null) { + if (location != "") + location += ", "; + location += " " + regionB.name; + } + if (location == "") + ai.print("trade route unable to get labor"); + else + ai.print("trade route waiting for labor at:" + location); + } + } + if (route.endpointA !is null && route.endpointB !is null && resources.canTradeBetween(route.endpointA, route.endpointB)) { + pendingRoutes.remove(route); + --i; --cnt; + if (log) + ai.print("trade route established between " + addrstr(route.territoryA) + " and " + addrstr(route.territoryB)); + } + //Perform routine duties + route.focusTick(ai, this, time); + } + } + + void registerOwnedSystemAdded(SystemAI& sysAI) { + auto@ sys = SystemCheck(this, sysAI); + checkedOwnedSystems.insertLast(sys); + if (log) + ai.print("adding owned system: " + sysAI.obj.name); + } + + void registerOwnedSystemRemoved(SystemAI& sysAI) { + for (uint i = 0, cnt = checkedOwnedSystems.length; i < cnt; ++i) { + if (sysAI is checkedOwnedSystems[i].ai) { + checkedOwnedSystems.removeAt(i); + break; + } + } + if (log) + ai.print("removing owned system: " + sysAI.obj.name); + } + + void registerOutsideBorderSystemAdded(SystemAI& sysAI) { + auto@ sys = SystemCheck(this, sysAI); + checkedOutsideSystems.insertLast(sys); + if (log) + ai.print("adding outside system: " + sysAI.obj.name); + } + + void registerOutsideBorderSystemRemoved(SystemAI& sysAI) { + for (uint i = 0, cnt = checkedOutsideSystems.length; i < cnt; ++i) { + if (sysAI is checkedOutsideSystems[i].ai) { + checkedOutsideSystems.removeAt(i); + break; + } + } + if (log) + ai.print("removing outside system: " + sysAI.obj.name); + } + + void registerPlanetAdded(PlanetAI& plAI) { + auto@ pl = PlanetCheck(this, plAI); + checkedPlanets.insertLast(pl); + if (log) + ai.print("adding planet: " + plAI.obj.name); + } + + void registerPlanetRemoved(PlanetAI& plAI) { + for (uint i = 0, cnt = checkedPlanets.length; i < cnt; ++i) { + if (plAI is checkedPlanets[i].ai) { + checkedPlanets.removeAt(i); + break; + } + } + if (log) + ai.print("removing planet: " + plAI.obj.name); + } + + void establishTradeRoute(Territory@ territoryA, Territory@ territoryB) { + if (canBuildGate) + return; + if (hasPendingTradeRoute(territoryA, territoryB)) { + if (log) + ai.print("pending route detected between " + addrstr(territoryA) + " and " + addrstr(territoryB) + ", establishment canceled"); + return; + } + + if (territoryA is null || territoryB is null) { + if (log) + ai.print("invalid territory for pending route, establishment canceled"); + return; + } + pendingRoutes.insertLast(TradeRoute(territoryA, territoryB)); + if (log) + ai.print("establishing trade route between " + addrstr(territoryA) + " and " + addrstr(territoryB)); + } + + SystemOrder@ requestOrbital(Region@ region, const OrbitalModule@ module, double priority = 1.0, double expires = INFINITY, uint moneyType = BT_Infrastructure) { + SystemAI@ sysAI = systems.getAI(region); + if (sysAI !is null) { + for (uint i = 0, cnt = checkedOwnedSystems.length; i < cnt; ++i) { + if (sysAI is checkedOwnedSystems[i].ai) + return checkedOwnedSystems[i].buildInSystem(this, module, priority, false, expires, moneyType); + } + ai.print("ERROR: requestOrbital: owned system not found: " + region.name); + return null; + } + return null; + } + + bool shouldHaveOutpost(SystemCheck& sys, SystemArea area, SystemBuildLocation&out loc) const { + loc = BL_InSystem; + + uint presentMask = sys.ai.seenPresent; + //Make sure we did not previously built an outpost here + if (orbitals.haveInSystem(ai.defs.TradeOutpost, sys.ai.obj)) + return false; + //Make sure we are not already building an outpost here + if (isBuilding(sys, ai.defs.TradeOutpost)) + return false; + //Hostile systems should be ignored until cleared + if (presentMask & ai.enemyMask != 0) + return false; + //Inhabited systems should be ignored if we're not aggressively expanding + if(!ai.behavior.colonizeNeutralOwnedSystems && (presentMask & ai.neutralMask) != 0) + return false; + if(!ai.behavior.colonizeAllySystems && (presentMask & ai.allyMask) != 0) + return false; + else { + Planet@ planet; + ResourceType@ type; + + switch(area) { + //Owned systems should have an outpost + case SA_Core: + if (sys.ai.planets.length > 0) + loc = BL_AtBestPlanet; + return true; + //Outside systems might have an outpost if they are of some interest + case SA_Tradable: + @planet = getBestPlanet(sys, type); + if (planet is null) + break; + loc = BL_AtBestPlanet; + //The best planet is barren, the system needs an outpost to allow expansion + if (int(planet.primaryResourceType) == -1) + return true; + //The best planet has either a scalable or level 3 or 2 resource, the system should have an outpost to dissuade other empires from colonizing it + if (type !is null && (type.cls is scalableClass || type.level == 3 || type.level == 2)) + return true; + return false; + default: + return false; + } + } + return false; + } + + bool shouldHaveMoonBase(PlanetCheck& pl) const { + if (pl.ai.obj.moonCount == 0) + return false; + //If the planet is at least level 2 and short on empty developed tiles, it should have a moon base + else if (pl.ai.obj.resourceLevel > 1 && pl.ai.obj.emptyDevelopedTiles < 9) + return true; + + return false; + } + + Region@ getRouteEndpoint(Territory@ territory) { + const OrbitalModule@ module = ai.defs.TradeStation; + Region@ region = null; + for (uint i = 0, cnt = module.ai.length; i < cnt; ++i) { + auto@ hook = cast(module.ai[i]); + if (hook !is null) { + Object@ obj = hook.considerBuild(orbitals, module, territory); + if (obj !is null) { + @region = cast(obj); + break; + } + } + } + return region; + } + + Region@ getLaborAt(Territory@ territory, double&out expires) { + expires = 600.0; + + if (territory is null) { + if (log) + ai.print("invalid territory to get labor at"); + return null; + } + //SoI - TODO: Handle more complex cases + + //Fallback solution: build a labor generation building + Planet@ pl = development.getLaborAt(territory, expires); + if (pl !is null) + return pl.region; + return null; + } + + bool isBuilding(const OrbitalModule@ module) { + for (uint i = 0, cnt = SystemCheck::allOrders.length; i < cnt; ++i) { + auto@ orbital = cast(SystemCheck::allOrders[i].info); + if (orbital !is null) { + if (orbital.module is module) + return true; + } + } + return false; + } + + bool isBuilding(SystemCheck& sys, const OrbitalModule@ module) { + for (uint i = 0, cnt = sys.orders.length; i < cnt; ++i) { + auto@ orbital = cast(sys.orders[i].info); + if (orbital !is null) { + if (orbital.module is module) + return true; + } + } + return false; + } + + bool isBuilding(const ConstructionType@ consType) { + for (uint i = 0, cnt = PlanetCheck::allOrders.length; i < cnt; ++i) { + auto@ generic = cast(PlanetCheck::allOrders[i].info); + if (generic !is null) { + if (generic.construction is consType) + return true; + } + } + return false; + } + + bool isBuilding(PlanetCheck& pl, const ConstructionType@ consType) { + for (uint i = 0, cnt = pl.orders.length; i < cnt; ++i) { + auto@ generic = cast(pl.orders[i].info); + if (generic !is null) { + if (generic.construction is consType) + return true; + } + } + return false; + } + + bool hasPendingTradeRoute(Territory@ territoryA, Territory@ territoryB) { + for (uint i = 0, cnt = pendingRoutes.length; i < cnt; ++i) { + if (pendingRoutes[i].territoryA is territoryA && pendingRoutes[i].territoryB is territoryB) + return true; + } + return false; + } + + Planet@ getBestPlanet(SystemCheck sys) { + ResourceType@ type; + return getBestPlanet(sys, type); + } + + Planet@ getBestPlanet(SystemCheck sys, const ResourceType@ resourceType) { + Planet@ bestPlanet, planet; + ResourcePreference bestResource = RP_None; + + if (sys.ai.obj is ai.empire.HomeSystem) { + //The homeworld if there is one + @planet = ai.empire.Homeworld; + if (planet !is null) + return planet; + } + + for (uint i = 0, cnt = sys.ai.planets.length; i < cnt; ++i) { + @planet = sys.ai.planets[i]; + int resId = planet.primaryResourceType; + if (resId == -1) + continue; + + const ResourceType@ type = getResource(resId); + //The first scalable resource + if (type.cls is scalableClass) { + @resourceType = type; + return planet; + } + //The first level 3 resource + if (type.level == 3) { + bestResource = RP_Level3; + @resourceType = type; + @bestPlanet = planet; + } + //The first level 2 resource + else if (type.level == 2 && RP_Level2 > bestResource) { + bestResource = RP_Level2; + @resourceType = type; + @bestPlanet = planet; + } + //The first level 1 resource + else if (type.level == 1 && RP_Level1 > bestResource) { + bestResource = RP_Level1; + @resourceType = type; + @bestPlanet = planet; + } + //The first level 0 resource except food and water + else if (type.level == 0 && type.cls !is foodClass && type.cls !is waterClass && RP_Level0 > bestResource) { + bestResource = RP_Level0; + @resourceType = type; + @bestPlanet = planet; + } + //The first food or water resource + else if ((type.cls is foodClass || type.cls is waterClass) && RP_Level0 > bestResource) { + bestResource = RP_FoodWater; + @resourceType = type; + @bestPlanet = planet; + } + else if (i == sys.ai.planets.length - 1 && bestPlanet is null) { + @resourceType = type; + @bestPlanet = planet; + } + } + + if (bestPlanet is null) + return planet; + return bestPlanet; + } +}; + +AIComponent@ createInfrastructure() { + return Infrastructure(); +} ADDED scripts/server/empire_ai/weasel/Intelligence.as Index: scripts/server/empire_ai/weasel/Intelligence.as ================================================================== --- scripts/server/empire_ai/weasel/Intelligence.as +++ scripts/server/empire_ai/weasel/Intelligence.as @@ -0,0 +1,542 @@ +// Intelligence +// ------------ +// Keeps track of the existence and movement of enemy fleets and other assets. +// + +import empire_ai.weasel.WeaselAI; +import empire_ai.weasel.Fleets; +import empire_ai.weasel.Systems; + +import regions.regions; + +final class FleetIntel { + Object@ obj; + bool known = false; + bool visible = false; + double lastSeen = 0; + double seenStrength = 0; + double predictStrength = 0; + + vec3d seenPosition; + Region@ seenRegion; + vec3d seenDestination; + Region@ seenTarget; + + void save(SaveFile& file) { + file << obj; + file << known; + file << visible; + file << lastSeen; + file << seenStrength; + file << predictStrength; + file << seenPosition; + file << seenRegion; + file << seenDestination; + file << seenTarget; + } + + void load(SaveFile& file) { + file >> obj; + file >> known; + file >> visible; + file >> lastSeen; + file >> seenStrength; + file >> predictStrength; + file >> seenPosition; + file >> seenRegion; + file >> seenDestination; + file >> seenTarget; + } + + bool get_isSignificant() { + return obj.getFleetStrength() > 0.1; + } + + bool tick(AI& ai, Intelligence& intelligence, Intel& intel) { + if(visible) { + if(!obj.valid || obj.owner !is intel.empire) + return false; + } + else { + if(!obj.valid || obj.owner !is intel.empire) { + if(!known || lastSeen < gameTime - 300.0) + return false; + } + } + if(obj.isVisibleTo(ai.empire)) { + known = true; + visible = true; + lastSeen = gameTime; + + seenStrength = obj.getFleetStrength(); + predictStrength = obj.getFleetMaxStrength(); + int supCap = obj.SupplyCapacity; + double fillPct = 1.0; + if(supCap != 0) { + double fillPct = double(obj.SupplyUsed) / double(supCap); + if(fillPct > 0.5) + predictStrength /= fillPct; + else + predictStrength *= 2.0; + } + + seenPosition = obj.position; + @seenRegion = obj.region; + + if(obj.isMoving) { + seenDestination = obj.computedDestination; + if(seenRegion !is null && inRegion(seenRegion, seenDestination)) + @seenTarget = seenRegion; + else if(seenTarget !is null && inRegion(seenTarget, seenDestination)) + @seenTarget = seenTarget; + else + @seenTarget = getRegion(seenDestination); + } + else { + seenDestination = seenPosition; + @seenTarget = seenRegion; + } + } + else { + visible = false; + } + return true; + } +}; + +final class Intel { + Empire@ empire; + uint borderedTo = 0; + + array fleets; + array shared; + array theirBorder; + array theirOwned; + + void save(Intelligence& intelligence, SaveFile& file) { + uint cnt = fleets.length; + file << cnt; + for(uint i = 0; i < cnt; ++i) + fleets[i].save(file); + + cnt = shared.length; + file << cnt; + for(uint i = 0; i < cnt; ++i) + intelligence.systems.saveAI(file, shared[i]); + + cnt = theirBorder.length; + file << cnt; + for(uint i = 0; i < cnt; ++i) + intelligence.systems.saveAI(file, theirBorder[i]); + + cnt = theirOwned.length; + file << cnt; + for(uint i = 0; i < cnt; ++i) + intelligence.systems.saveAI(file, theirOwned[i]); + + file << borderedTo; + } + + void load(Intelligence& intelligence, SaveFile& file) { + uint cnt = 0; + + file >> cnt; + for(uint i = 0; i < cnt; ++i) { + FleetIntel flIntel; + flIntel.load(file); + if(flIntel.obj !is null) + fleets.insertLast(flIntel); + } + + file >> cnt; + for(uint i = 0; i < cnt; ++i) { + auto@ sys = intelligence.systems.loadAI(file); + if(sys !is null) + shared.insertLast(sys); + } + + file >> cnt; + for(uint i = 0; i < cnt; ++i) { + auto@ sys = intelligence.systems.loadAI(file); + if(sys !is null) + theirBorder.insertLast(sys); + } + + file >> cnt; + for(uint i = 0; i < cnt; ++i) { + auto@ sys = intelligence.systems.loadAI(file); + if(sys !is null) + theirOwned.insertLast(sys); + } + + file >> borderedTo; + } + + //TODO: If a fleet is going to drop out of cutoff range soon, + // queue up a scouting mission to its last known position so we + // can try to regain intel on it. + + double getSeenStrength(double cutOff = 600.0) { + double total = 0.0; + cutOff = gameTime - cutOff; + for(uint i = 0, cnt = fleets.length; i < cnt; ++i) { + auto@ flIntel = fleets[i]; + if(!flIntel.known) + continue; + if(flIntel.lastSeen < cutOff) + continue; + total += sqrt(fleets[i].seenStrength); + } + return total * total; + } + + double getPredictiveStrength(double cutOff = 600.0) { + double total = 0.0; + cutOff = gameTime - cutOff; + for(uint i = 0, cnt = fleets.length; i < cnt; ++i) { + auto@ flIntel = fleets[i]; + if(!flIntel.known) + continue; + if(flIntel.lastSeen < cutOff) + continue; + total += sqrt(fleets[i].predictStrength); + } + return total * total; + } + + double accuracy(AI& ai, Intelligence& intelligence, double cutOff = 600.0) { + uint total = 0; + uint known = 0; + + cutOff = gameTime - cutOff; + for(uint i = 0, cnt = fleets.length; i < cnt; ++i) { + auto@ flIntel = fleets[i]; + if(!flIntel.isSignificant) + continue; + + total += 1; + if(flIntel.known && flIntel.lastSeen >= cutOff) + known += 1; + } + + if(total == 0) + return 1.0; + return double(known) / double(total); + } + + double defeatability(AI& ai, Intelligence& intelligence, double cutOff = 600.0) { + double acc = accuracy(ai, intelligence, cutOff); + double ourStrength = 0, theirStrength = 0; + + if(acc < 0.6) { + //In low-accuracy situations, base it on the empire overall strength metric + theirStrength = empire.TotalMilitary; + ourStrength = ai.empire.TotalMilitary; + } + else { + theirStrength = getPredictiveStrength(cutOff * 10.0); + ourStrength = intelligence.fleets.totalStrength; + } + + if(theirStrength == 0) + return 10.0; + return ourStrength / theirStrength; + } + + void tick(AI& ai, Intelligence& intelligence) { + //Keep known fleets updated + for(uint i = 0, cnt = fleets.length; i < cnt; ++i) { + if(!fleets[i].tick(ai, intelligence, this)) { + fleets.removeAt(i); + --i; --cnt; + } + } + } + + bool isShared(AI& ai, SystemAI@ sys) { + return sys.seenPresent & ai.empire.mask != 0 && sys.seenPresent & empire.mask != 0; + } + + bool isBorder(AI& ai, SystemAI@ sys) { + return sys.outsideBorder && sys.seenPresent & empire.mask != 0; + } + + void focusTick(AI& ai, Intelligence& intelligence) { + //Detect newly created fleets + auto@ data = empire.getFlagships(); + Object@ obj; + while(receive(data, obj)) { + if(obj is null) + continue; + + bool found = false; + for(uint i = 0, cnt = fleets.length; i < cnt; ++i) { + if(fleets[i].obj is obj) { + found = true; + break; + } + } + + if(!found) { + FleetIntel flIntel; + @flIntel.obj = obj; + fleets.insertLast(flIntel); + } + } + + //Remove no longer shared and border systems + for(uint i = 0, cnt = shared.length; i < cnt; ++i) { + if(!isShared(ai, shared[i])) { + shared.removeAt(i); + --i; --cnt; + } + } + for(uint i = 0, cnt = theirBorder.length; i < cnt; ++i) { + if(!isBorder(ai, theirBorder[i])) { + theirBorder.removeAt(i); + --i; --cnt; + } + } + + borderedTo = 0; + for(uint i = 0, cnt = theirOwned.length; i < cnt; ++i) { + auto@ sys = theirOwned[i]; + uint seen = sys.seenPresent; + if(seen & empire.mask == 0) { + theirOwned.removeAt(i); + --i; --cnt; + continue; + } + + for(uint n = 0, ncnt = sys.desc.adjacent.length; n < ncnt; ++n) { + auto@ other = intelligence.systems.getAI(sys.desc.adjacent[n]); + if(other !is null) + borderedTo |= other.seenPresent & ~empire.mask; + } + + for(uint n = 0, ncnt = sys.desc.wormholes.length; n < ncnt; ++n) { + auto@ other = intelligence.systems.getAI(sys.desc.wormholes[n]); + if(other !is null) + borderedTo |= other.seenPresent & ~empire.mask; + } + } + + //Detect shared and border systems + for(uint i = 0, cnt = intelligence.systems.owned.length; i < cnt; ++i) { + auto@ sys = intelligence.systems.owned[i]; + if(isShared(ai, sys)) { + if(shared.find(sys) == -1) + shared.insertLast(sys); + } + } + for(uint i = 0, cnt = intelligence.systems.outsideBorder.length; i < cnt; ++i) { + auto@ sys = intelligence.systems.outsideBorder[i]; + if(isBorder(ai, sys)) { + if(theirBorder.find(sys) == -1) + theirBorder.insertLast(sys); + } + } + for(uint i = 0, cnt = intelligence.systems.all.length; i < cnt; ++i) { + auto@ sys = intelligence.systems.all[i]; + if(sys.seenPresent & empire.mask != 0) { + if(theirOwned.find(sys) == -1) + theirOwned.insertLast(sys); + } + } + + //Try to update some stuff + SystemAI@ check; + double lru = 0; + + for(uint i = 0, cnt = shared.length; i < cnt; ++i) { + auto@ sys = shared[i]; + double update = sys.lastStrengthCheck; + if(update < lru && sys.visible) { + @check = sys; + lru = update; + } + } + + for(uint i = 0, cnt = theirBorder.length; i < cnt; ++i) { + auto@ sys = theirBorder[i]; + double update = sys.lastStrengthCheck; + if(update < lru && sys.visible) { + @check = sys; + lru = update; + } + } + + if(check !is null) + check.strengthCheck(ai); + } +}; + +class Intelligence : AIComponent { + Fleets@ fleets; + Systems@ systems; + + array intel; + + void create() { + @fleets = cast(ai.fleets); + @systems = cast(ai.systems); + } + + void start() { + for(uint i = 0, cnt = getEmpireCount(); i < cnt; ++i) { + Empire@ emp = getEmpire(i); + if(emp is ai.empire) + continue; + if(!emp.major) + continue; + + Intel empIntel; + @empIntel.empire = emp; + + if(intel.length <= uint(emp.index)) + intel.length = uint(emp.index)+1; + @intel[emp.index] = empIntel; + } + } + + void save(SaveFile& file) { + uint cnt = intel.length; + file << cnt; + for(uint i = 0; i < cnt; ++i) { + if(intel[i] is null) { + file.write0(); + continue; + } + + file.write1(); + intel[i].save(this, file); + } + } + + void load(SaveFile& file) { + uint cnt = 0; + file >> cnt; + intel.length = cnt; + + for(uint i = 0; i < cnt; ++i) { + if(!file.readBit()) + continue; + + @intel[i] = Intel(); + @intel[i].empire = getEmpire(i); + intel[i].load(this, file); + } + } + + Intel@ get(Empire@ emp) { + if(emp is null) + return null; + if(!emp.major) + return null; + if(uint(emp.index) >= intel.length) + return null; + return intel[emp.index]; + } + + uint ind = 0; + void tick(double time) override { + if(intel.length == 0) + return; + ind = (ind+1)%intel.length; + if(intel[ind] !is null) + intel[ind].tick(ai, this); + } + + uint fInd = 0; + void focusTick(double time) override { + if(intel.length == 0) + return; + fInd = (fInd+1)%intel.length; + if(intel[fInd] !is null) + intel[fInd].focusTick(ai, this); + } + + string strdisplay(double str) { + return standardize(str * 0.001, true); + } + + double defeatability(Empire@ emp) { + auto@ empIntel = get(emp); + if(empIntel is null) + return 0.0; + return empIntel.defeatability(ai, this); + } + + double defeatability(uint theirMask, uint myMask = 0, double cutOff = 600.0) { + if(myMask == 0) + myMask = ai.empire.mask; + + double minAcc = 1.0; + for(uint i = 0, cnt = intel.length; i < cnt; ++i) { + auto@ itl = intel[i]; + if(itl is null || itl.empire is null) + continue; + if((theirMask | myMask) & itl.empire.mask == 0) + continue; + minAcc = min(itl.accuracy(ai, this, cutOff), minAcc); + } + + double ourStrength = 0, theirStrength = 0; + for(uint i = 0, cnt = intel.length; i < cnt; ++i) { + auto@ itl = intel[i]; + if(itl is null || itl.empire is null) + continue; + if((theirMask | myMask) & itl.empire.mask == 0) + continue; + + double str = 0.0; + if(minAcc < 0.6) + str = itl.empire.TotalMilitary; + else + str = itl.getPredictiveStrength(cutOff * 10.0); + + if(itl.empire.mask & theirMask != 0) + theirStrength += str; + if(itl.empire.mask & myMask != 0) + ourStrength += str; + } + + if(myMask & ai.empire.mask != 0) { + if(minAcc < 0.6) + ourStrength += ai.empire.TotalMilitary; + else + ourStrength += fleets.totalStrength; + } + if(theirMask & ai.empire.mask != 0) { + if(minAcc < 0.6) + theirStrength += ai.empire.TotalMilitary; + else + theirStrength += fleets.totalStrength; + } + + if(theirStrength == 0) + return 10.0; + return ourStrength / theirStrength; + } + + void turn() override { + if(log) { + ai.print("Intelligence Report on Empires:"); + ai.print(ai.pad(" Our strength: ", 18)+strdisplay(fleets.totalStrength)+" / "+strdisplay(fleets.totalMaxStrength)); + for(uint i = 0, cnt = intel.length; i < cnt; ++i) { + auto@ empIntel = intel[i]; + if(empIntel is null) + continue; + ai.print(" "+ai.pad(empIntel.empire.name, 16) + +ai.pad(" "+strdisplay(empIntel.getSeenStrength()) + +" / "+strdisplay(empIntel.getPredictiveStrength()), 20) + +" defeatability "+toString(empIntel.defeatability(ai, this), 2) + +" accuracy "+toString(empIntel.accuracy(ai, this), 2)); + } + } + } +}; + +AIComponent@ createIntelligence() { + return Intelligence(); +} ADDED scripts/server/empire_ai/weasel/Military.as Index: scripts/server/empire_ai/weasel/Military.as ================================================================== --- scripts/server/empire_ai/weasel/Military.as +++ scripts/server/empire_ai/weasel/Military.as @@ -0,0 +1,749 @@ +// Military +// -------- +// Military construction logic. Builds and restores fleets and defensive stations, +// but does not deal with actually using those fleets to fight - that is the purview of +// the War component. +// + +import empire_ai.weasel.WeaselAI; + +import empire_ai.weasel.Designs; +import empire_ai.weasel.Construction; +import empire_ai.weasel.Budget; +import empire_ai.weasel.Fleets; +import empire_ai.weasel.Development; +import empire_ai.weasel.Movement; +import empire_ai.weasel.Systems; +import empire_ai.weasel.Orbitals; + +import resources; + +class SupportOrder { + DesignTarget@ design; + Object@ onObject; + AllocateBudget@ alloc; + bool isGhostOrder = false; + double expires = INFINITY; + uint count = 0; + + void save(Military& military, SaveFile& file) { + military.designs.saveDesign(file, design); + file << onObject; + military.budget.saveAlloc(file, alloc); + file << isGhostOrder; + file << expires; + file << count; + } + + void load(Military& military, SaveFile& file) { + @design = military.designs.loadDesign(file); + file >> onObject; + @alloc = military.budget.loadAlloc(file); + file >> isGhostOrder; + file >> expires; + file >> count; + } + + bool tick(AI& ai, Military& military, double time) { + if(alloc !is null) { + if(alloc.allocated) { + if(isGhostOrder) + onObject.rebuildAllGhosts(); + else + onObject.orderSupports(design.active.mostUpdated(), count); + if(military.log && design !is null) + ai.print("Support order completed for "+count+"x "+design.active.name+" ("+design.active.size+")", onObject); + return false; + } + } + else if(design !is null) { + if(design.active !is null) + @alloc = military.budget.allocate(BT_Military, getBuildCost(design.active.mostUpdated()) * count); + } + if(expires < gameTime) { + if(alloc !is null && !alloc.allocated) + military.budget.remove(alloc); + if(isGhostOrder) + onObject.clearAllGhosts(); + if(military.log) + ai.print("Support order expired", onObject); + return false; + } + return true; + } +}; + +class StagingBase { + Region@ region; + array fleets; + + double idleTime = 0.0; + double occupiedTime = 0.0; + + OrbitalAI@ shipyard; + BuildOrbital@ shipyardBuild; + Factory@ factory; + + bool isUnderAttack = false; + + void save(Military& military, SaveFile& file) { + file << region; + file << idleTime; + file << occupiedTime; + file << isUnderAttack; + + military.orbitals.saveAI(file, shipyard); + military.construction.saveConstruction(file, shipyardBuild); + + uint cnt = fleets.length; + file << cnt; + for(uint i = 0; i < cnt; ++i) + military.fleets.saveAI(file, fleets[i]); + } + + void load(Military& military, SaveFile& file) { + file >> region; + file >> idleTime; + file >> occupiedTime; + file >> isUnderAttack; + + @shipyard = military.orbitals.loadAI(file); + @shipyardBuild = cast(military.construction.loadConstruction(file)); + + uint cnt = 0; + file >> cnt; + for(uint i = 0; i < cnt; ++i) { + if(i > 200 && file < SV_0158) { + //Something went preeeetty wrong in an old save + if(file.readBit()) { + Object@ obj; + file >> obj; + } + } + else { + auto@ fleet = military.fleets.loadAI(file); + if(fleet !is null) + fleets.insertLast(fleet); + } + } + } + + bool tick(AI& ai, Military& military, double time) { + if(fleets.length == 0) { + occupiedTime = 0.0; + idleTime += time; + } + else { + occupiedTime += time; + idleTime = 0.0; + } + + isUnderAttack = region.ContestedMask & ai.mask != 0; + + //Manage building our shipyard + if(shipyardBuild !is null) { + if(shipyardBuild.completed) { + @shipyard = military.orbitals.getInSystem(ai.defs.Shipyard, region); + if(shipyard !is null) + @shipyardBuild = null; + } + } + if(shipyard !is null) { + if(!shipyard.obj.valid) { + @shipyard = null; + @shipyardBuild = null; + } + } + + if(factory !is null && (!factory.valid || factory.obj.region !is region)) + @factory = null; + if(factory is null) + @factory = military.construction.getFactory(region); + + if(factory !is null) { + factory.needsSupportLabor = false; + factory.waitingSupportLabor = 0.0; + if(factory.obj.hasOrderedSupports) { + factory.needsSupportLabor = true; + factory.waitingSupportLabor += double(factory.obj.SupplyOrdered) * ai.behavior.estSizeSupportLabor; + } + for(uint i = 0, cnt = fleets.length; i < cnt; ++i) { + if(fleets[i].isHome && fleets[i].obj.hasOrderedSupports) { + factory.needsSupportLabor = true; + factory.waitingSupportLabor += double(fleets[i].obj.SupplyOrdered) * ai.behavior.estSizeSupportLabor; + break; + } + } + if(factory.waitingSupportLabor > 0) + factory.aimForLabor(factory.waitingSupportLabor / ai.behavior.constructionMaxTime); + } + + bool isFactorySufficient = false; + if(factory !is null) { + if(factory.waitingSupportLabor <= factory.laborIncome * ai.behavior.constructionMaxTime + || factory.obj.canImportLabor || factory !is military.construction.primaryFactory) + isFactorySufficient = true; + } + + for(uint i = 0, cnt = fleets.length; i < cnt; ++i) { + Object@ obj = fleets[i].obj; + if(obj is null || !obj.valid) { + fleets.removeAt(i); + --i; --cnt; + continue; + } + fleets[i].stationedFactory = isFactorySufficient; + } + + if(occupiedTime >= 3.0 * 60.0 && ai.defs.Shipyard !is null && shipyard is null && shipyardBuild is null + && !isUnderAttack && (!isFactorySufficient && factory !is military.construction.primaryFactory)) { + //If any fleets need construction try to queue up a shipyard + bool needYard = false; + for(uint i = 0, cnt = fleets.length; i < cnt; ++i) { + auto@ flt = fleets[i]; + if(flt.obj.hasOrderedSupports || flt.filled < 0.8) { + needYard = true; + break; + } + } + + if(needYard) { + @shipyard = military.orbitals.getInSystem(ai.defs.Shipyard, region); + if(shipyard is null) { + vec3d pos = region.position; + vec2d offset = random2d(region.radius * 0.4, region.radius * 0.8); + pos.x += offset.x; + pos.z += offset.y; + + @shipyardBuild = military.construction.buildOrbital(ai.defs.Shipyard, pos); + } + } + } + + if((idleTime >= 10.0 * 60.0 || region.PlanetsMask & ai.mask == 0) && (shipyardBuild is null || shipyard !is null) && (factory is null || (shipyard !is null && factory.obj is shipyard.obj)) && military.stagingBases.length >= 2) { + if(shipyard !is null && !ai.behavior.forbidScuttle) { + cast(shipyard.obj).scuttle(); + } + else { + if(factory !is null) { + factory.needsSupportLabor = false; + @factory = null; + } + return false; + } + } + return true; + } +}; + +class Military : AIComponent { + Fleets@ fleets; + Development@ development; + Designs@ designs; + Construction@ construction; + Budget@ budget; + Systems@ systems; + Orbitals@ orbitals; + + array supportOrders; + array stagingBases; + + AllocateConstruction@ mainWait; + bool spentMoney = true; + + void create() { + @fleets = cast(ai.fleets); + @development = cast(ai.development); + @designs = cast(ai.designs); + @construction = cast(ai.construction); + @budget = cast(ai.budget); + @systems = cast(ai.systems); + @orbitals = cast(ai.orbitals); + } + + void save(SaveFile& file) { + uint cnt = supportOrders.length; + file << cnt; + for(uint i = 0; i < cnt; ++i) + supportOrders[i].save(this, file); + + construction.saveConstruction(file, mainWait); + file << spentMoney; + + cnt = stagingBases.length; + file << cnt; + for(uint i = 0; i < cnt; ++i) { + saveStaging(file, stagingBases[i]); + stagingBases[i].save(this, file); + } + } + + void load(SaveFile& file) { + uint cnt = 0; + file >> cnt; + for(uint i = 0; i < cnt; ++i) { + SupportOrder ord; + ord.load(this, file); + if(ord.onObject !is null) + supportOrders.insertLast(ord); + } + + @mainWait = construction.loadConstruction(file); + file >> spentMoney; + + file >> cnt; + for(uint i = 0; i < cnt; ++i) { + StagingBase@ base = loadStaging(file); + if(base !is null) { + base.load(this, file); + if(stagingBases.find(base) == -1) + stagingBases.insertLast(base); + } + else { + StagingBase().load(this, file); + } + } + } + + void loadFinalize(AI& ai) override { + for(uint i = 0, cnt = stagingBases.length; i < cnt; ++i) { + auto@ base = stagingBases[i]; + for(uint n = 0, ncnt = base.fleets.length; n < ncnt; ++n) { + Object@ obj = base.fleets[n].obj; + if(obj is null || !obj.valid || !obj.initialized) { + base.fleets.removeAt(n); + --n; --ncnt; + } + } + } + } + + StagingBase@ loadStaging(SaveFile& file) { + Region@ reg; + file >> reg; + + if(reg is null) + return null; + + StagingBase@ base = getBase(reg); + if(base is null) { + @base = StagingBase(); + @base.region = reg; + stagingBases.insertLast(base); + } + return base; + } + + void saveStaging(SaveFile& file, StagingBase@ base) { + Region@ reg; + if(base !is null) + @reg = base.region; + file << reg; + } + + Region@ getClosestStaging(Region& targetRegion) { + //Check if we have anything close enough + StagingBase@ best; + int minHops = INT_MAX; + for(uint i = 0, cnt = stagingBases.length; i < cnt; ++i) { + int d = systems.hopDistance(stagingBases[i].region, targetRegion); + if(d < minHops) { + minHops = d; + @best = stagingBases[i]; + } + } + if(best !is null) + return best.region; + return null; + } + + Region@ getStagingFor(Region& targetRegion) { + //Check if we have anything close enough + StagingBase@ best; + int minHops = INT_MAX; + for(uint i = 0, cnt = stagingBases.length; i < cnt; ++i) { + int d = systems.hopDistance(stagingBases[i].region, targetRegion); + if(d < minHops) { + minHops = d; + @best = stagingBases[i]; + } + } + if(minHops < ai.behavior.stagingMaxHops) + return best.region; + + //Create a new staging base for this + Region@ bestNew; + minHops = INT_MAX; + for(uint i = 0, cnt = systems.border.length; i < cnt; ++i) { + auto@ sys = systems.border[i].obj; + int d = systems.hopDistance(sys, targetRegion); + if(d < minHops) { + minHops = d; + @bestNew = sys; + } + } + + if(minHops > ai.behavior.stagingMaxHops && best !is null) + return best.region; + + auto@ base = getBase(bestNew); + if(base !is null) + return base.region; + else + return createStaging(bestNew).region; + } + + StagingBase@ createStaging(Region@ region) { + if(region is null) + return null; + + if(log) + ai.print("Create new staging base.", region); + + StagingBase newBase; + @newBase.region = region; + + for(uint i = 0, cnt = fleets.fleets.length; i < cnt; ++i) { + if(fleets.fleets[i].stationed is region) + newBase.fleets.insertLast(fleets.fleets[i]); + } + + stagingBases.insertLast(newBase); + return newBase; + } + + StagingBase@ getBase(Region@ inRegion) { + if(inRegion is null) + return null; + for(uint i = 0, cnt = stagingBases.length; i < cnt; ++i) { + if(stagingBases[i].region is inRegion) + return stagingBases[i]; + } + return null; + } + + vec3d getStationPosition(Region& inRegion, double distance = 100.0) { + auto@ base = getBase(inRegion); + if(base !is null) { + if(base.shipyard !is null) { + vec3d pos = base.shipyard.obj.position; + vec2d offset = random2d(distance * 0.5, distance * 1.5); + pos.x += offset.x; + pos.z += offset.y; + + return pos; + } + } + + vec3d pos = inRegion.position; + vec2d offset = random2d(inRegion.radius * 0.4, inRegion.radius * 0.8); + pos.x += offset.x; + pos.z += offset.y; + return pos; + } + + void stationFleet(FleetAI@ fleet, Region@ inRegion) { + if(inRegion is null || fleet.stationed is inRegion) + return; + + auto@ prevBase = getBase(fleet.stationed); + if(prevBase !is null) + prevBase.fleets.remove(fleet); + + auto@ base = getBase(inRegion); + if(base !is null) + base.fleets.insertLast(fleet); + + @fleet.stationed = inRegion; + fleet.stationedFactory = construction.getFactory(inRegion) !is null; + if(fleet.mission is null) + fleets.returnToBase(fleet); + } + + void orderSupportsOn(Object& obj, double expire = 60.0) { + if(obj.SupplyGhost > 0) { + if(ai.behavior.fleetsRebuildGhosts) { + //Try to rebuild the fleet's ghosts + SupportOrder ord; + @ord.onObject = obj; + @ord.alloc = budget.allocate(BT_Military, obj.rebuildGhostsCost()); + ord.expires = gameTime + expire; + ord.isGhostOrder = true; + + supportOrders.insertLast(ord); + + if(log) + ai.print("Attempting to rebuild ghosts", obj); + return; + } + else { + obj.clearAllGhosts(); + } + } + + int supCap = obj.SupplyCapacity; + int supHave = obj.SupplyUsed - obj.SupplyGhost; + + //Build some supports + int supSize = pow(2, round(::log(double(supCap) * randomd(0.005, 0.03))/::log(2.0))); + supSize = max(min(supSize, supCap - supHave), 1); + + SupportOrder ord; + @ord.onObject = obj; + @ord.design = designs.design(DP_Support, supSize); + ord.expires = gameTime + expire; + ord.count = clamp((supCap - supHave)/supSize, 1, int(ceil((randomd(0.01, 0.1)*supCap)/double(supSize)))); + + if(log) + ai.print("Attempting to build supports: "+ord.count+"x size "+supSize, obj); + + supportOrders.insertLast(ord); + } + + void findSomethingToDo() { + //See if we should retrofit anything + if(mainWait is null && !spentMoney && gameTime > ai.behavior.flagshipBuildMinGameTime) { + int availMoney = budget.spendable(BT_Military); + int moneyTargetSize = floor(double(availMoney) * ai.behavior.shipSizePerMoney); + + //See if one of our fleets is old enough that we can retrofit it + for(uint i = 0, cnt = fleets.fleets.length; i < cnt; ++i) { + FleetAI@ fleet = fleets.fleets[i]; + if(fleet.mission !is null && fleet.mission.isActive) + continue; + if(fleet.fleetClass != FC_Combat) + continue; + if(fleet.obj.hasOrders) + continue; + + Ship@ ship = cast(fleet.obj); + if(ship is null) + continue; + + //Don't retrofit free fleets + if(ship.isFree && !ai.behavior.retrofitFreeFleets) + continue; + + //Find the factory assigned to this + Factory@ factory; + if(fleet.isHome) { + Region@ reg = fleet.obj.region; + @factory = construction.getFactory(reg); + } + if(factory is null) + continue; + if(factory.busy) + continue; + + //Find how large we can make this flagship + const Design@ dsg = ship.blueprint.design; + int targetSize = min(int(moneyTargetSize * 1.2), int(factory.laborToBear(ai) * 1.3 * ai.behavior.shipSizePerLabor)); + targetSize = 5 * floor(double(targetSize) / 5.0); + + //See if we should retrofit this + int size = ship.blueprint.design.size; + if(size > targetSize) + continue; + + double pctDiff = (double(targetSize) / double(size)) - 1.0; + if(pctDiff < ai.behavior.shipRetrofitThreshold) + continue; + + DesignTarget@ newDesign = designs.scale(dsg, targetSize); + spentMoney = true; + + auto@ retrofit = construction.retrofit(ship); + @mainWait = construction.buildNow(retrofit, factory); + + if(log) + ai.print("Retrofitting to size "+targetSize, fleet.obj); + + //TODO: This should mark the fleet as occupied for missions while we retrofit + + return; + } + + //See if we should build a new fleet + Factory@ factory = construction.primaryFactory; + if(factory !is null && !factory.busy) { + //Figure out how large our flagship would be if we built one + factory.aimForLabor((double(moneyTargetSize) / ai.behavior.shipSizePerLabor) / ai.behavior.constructionMaxTime); + int targetSize = min(moneyTargetSize, int(factory.laborToBear(ai) * ai.behavior.shipSizePerLabor)); + targetSize = 5 * floor(double(targetSize) / 5.0); + + int expMaint = double(targetSize) * ai.behavior.maintenancePerShipSize; + int expCost = double(targetSize) / ai.behavior.shipSizePerMoney; + if(budget.canSpend(BT_Military, expCost, expMaint)) { + //Make sure we're building an adequately sized flagship + uint count = 0; + double avgSize = 0.0; + for(uint i = 0, cnt = fleets.fleets.length; i < cnt; ++i) { + FleetAI@ fleet = fleets.fleets[i]; + Ship@ ship = cast(fleet.obj); + if(ship !is null && fleet.fleetClass == FC_Combat) { + avgSize += ship.blueprint.design.size; + count += 1; + } + } + if(count != 0) + avgSize /= double(count); + + if(count < ai.behavior.maxActiveFleets && targetSize >= avgSize * ai.behavior.flagshipBuildMinAvgSize) { + //Build the flagship + DesignTarget@ design = designs.design(DP_Combat, targetSize, + availMoney, budget.maintainable(BT_Military), + factory.laborToBear(ai), + findSize=true); + @mainWait = construction.buildFlagship(design); + mainWait.maxTime *= 1.5; + spentMoney = true; + + if(log) + ai.print("Ordering a new fleet at size "+targetSize); + + return; + } + } + } + } + + //See if any of our fleets need refilling + //TODO: Aim for labor on the factory so that the supports are built in reasonable time + for(uint i = 0, cnt = fleets.fleets.length; i < cnt; ++i) { + FleetAI@ fleet = fleets.fleets[i]; + if(fleet.mission !is null && fleet.mission.isActive) + continue; + if(fleet.fleetClass != FC_Combat) + continue; + if(fleet.obj.hasOrders) + continue; + if(fleet.filled >= 1.0) + continue; + if(hasSupportOrderFor(fleet.obj)) + continue; + if(!fleet.isHome) + continue; + + //Re-station to our factory if we're idle and need refill without being near a factory + Factory@ f = construction.getFactory(fleet.obj.region); + if(f is null) { + if(fleet.filled < ai.behavior.stagingToFactoryFill && construction.primaryFactory !is null) + stationFleet(fleet, construction.primaryFactory.obj.region); + continue; + } + + //Don't order if the factory has support orders, it'll just make everything take longer + if(f !is null && ai.behavior.supportOrderWaitOnFactory && fleet.filled < 0.9 && fleet.obj.SupplyGhost == 0) { + if(f.obj.hasOrderedSupports && f.obj.SupplyUsed < f.obj.SupplyCapacity) + continue; + } + + int supCap = fleet.obj.SupplyCapacity; + int supHave = fleet.obj.SupplyUsed - fleet.obj.SupplyGhost; + if(supHave < supCap) { + orderSupportsOn(fleet.obj); + spentMoney = true; + return; + } + } + + budget.checkedMilitarySpending = spentMoney; + + //TODO: Build defense stations + } + + bool hasSupportOrderFor(Object& obj) { + for(uint i = 0, cnt = supportOrders.length; i < cnt; ++i) { + if(supportOrders[i].onObject is obj) + return true; + } + return false; + } + + void tick(double time) override { + //Manage our orders for support ships + for(uint i = 0, cnt = supportOrders.length; i < cnt; ++i) { + if(!supportOrders[i].tick(ai, this, time)) { + supportOrders.removeAt(i); + --i; --cnt; + } + } + } + + void focusTick(double time) override { + //Find something for us to do + findSomethingToDo(); + + //If we're far into the budget, spend our money on building supports at our factories + if(budget.Progress > 0.9 && budget.canSpend(BT_Military, 10)) { + for(uint i = 0, cnt = construction.factories.length; i < cnt; ++i) { + //TODO: Build on planets in the system if this is full + auto@ f = construction.factories[i]; + if(f.obj.SupplyUsed < f.obj.SupplyCapacity && !hasSupportOrderFor(f.obj)) { + orderSupportsOn(f.obj, expire=budget.RemainingTime); + break; + } + } + } + + //Check if we should re-station any of our fleets + for(uint i = 0, cnt = fleets.fleets.length; i < cnt; ++i) { + auto@ flAI = fleets.fleets[i]; + if(flAI.stationed is null) { + Region@ reg = flAI.obj.region; + if(reg !is null && reg.PlanetsMask & ai.mask != 0) + stationFleet(flAI, reg); + } + } + + //Make sure all our major factories are considered staging bases + for(uint i = 0, cnt = construction.factories.length; i < cnt; ++i) { + auto@ f = construction.factories[i]; + if(f.obj.isShip) + continue; + Region@ reg = f.obj.region; + if(reg is null) + continue; + auto@ base = getBase(reg); + if(base is null) + createStaging(reg); + } + + //If we don't have any staging bases, make one at a focus + if(stagingBases.length == 0 && development.focuses.length != 0) { + Region@ reg = development.focuses[0].obj.region; + if(reg !is null) + createStaging(reg); + } + + //Update our staging bases + for(uint i = 0, cnt = stagingBases.length; i < cnt; ++i) { + auto@ base = stagingBases[i]; + if(!base.tick(ai, this, time)) { + stagingBases.removeAt(i); + --i; --cnt; + } + } + } + + void turn() override { + //Fleet construction happens in the beginning of the turn, because we want + //to use our entire military budget on it. + if(mainWait !is null) { + if(mainWait.completed) { + @mainWait = null; + } + else if(!mainWait.started) { + if(log) + ai.print("Failed current main construction wait."); + construction.cancel(mainWait); + @mainWait = null; + } + } + spentMoney = false; + } +}; + +AIComponent@ createMilitary() { + return Military(); +} ADDED scripts/server/empire_ai/weasel/Movement.as Index: scripts/server/empire_ai/weasel/Movement.as ================================================================== --- scripts/server/empire_ai/weasel/Movement.as +++ scripts/server/empire_ai/weasel/Movement.as @@ -0,0 +1,354 @@ +// Movement +// -------- +// Manages FTL travel modes, expenditure of FTL energy, and general movement patterns. +// + +import empire_ai.weasel.WeaselAI; + +import oddity_navigation; +import ftl; + +enum FTLReturn { + F_Pass, + F_Continue, + F_Done, + F_Kill, +}; + +class FTL : AIComponent { + uint order(MoveOrder& order) { return F_Pass; } + uint tick(MoveOrder& order, double time) { return F_Pass; } +}; + +bool getNearPosition(Object& obj, Object& target, vec3d& pos, bool spread = false) { + if(target !is null) { + if(target.isPlanet) { + Planet@ toPl = cast(target); + vec3d dir = obj.position - toPl.position; + dir = dir.normalized(toPl.OrbitSize * 0.9); + if(spread) + dir = quaterniond_fromAxisAngle(vec3d_up(), randomd(-0.15,0.15) * pi) * dir; + pos = toPl.position + dir; + return true; + } + else if(obj.hasLeaderAI && (target.isShip || target.isOrbital)) { + vec3d dir = obj.position - target.position; + dir = dir.normalized(obj.getEngagementRange()); + pos = target.position + dir; + return true; + } + else { + Region@ reg = cast(target); + if(reg is null) + @reg = target.region; + if(reg !is null) { + vec3d dir = obj.position - reg.position; + dir = dir.normalized(reg.radius * 0.85); + if(spread) + dir = quaterniond_fromAxisAngle(vec3d_up(), randomd(-0.15,0.15) * pi) * dir; + pos = reg.position + dir; + return true; + } + } + } + return false; +} + +bool targetPosition(MoveOrder& ord, vec3d& toPosition) { + if(ord.target !is null) { + return getNearPosition(ord.obj, ord.target, toPosition); + } + else { + toPosition = ord.position; + return true; + } +} + +double usableFTL(AI& ai, MoveOrder& ord) { + double storage = ai.empire.FTLCapacity; + double avail = ai.empire.FTLStored; + + double reserved = 0.0; + if(ord.priority < MP_Critical) + reserved += ai.behavior.ftlReservePctCritical; + if(ord.priority < MP_Normal) + reserved += ai.behavior.ftlReservePctNormal; + avail -= reserved * storage; + + return avail; +} + +enum MovePriority { + MP_Background, + MP_Normal, + MP_Critical +}; + +class MoveOrder { + int id = -1; + uint priority = MP_Normal; + Object@ obj; + Object@ target; + vec3d position; + bool completed = false; + bool failed = false; + + void save(Movement& movement, SaveFile& file) { + file << priority; + file << obj; + file << target; + file << position; + file << completed; + file << failed; + } + + void load(Movement& movement, SaveFile& file) { + file >> priority; + file >> obj; + file >> target; + file >> position; + file >> completed; + file >> failed; + } + + void cancel() { + failed = true; + obj.clearOrders(); + } + + bool tick(AI& ai, Movement& movement, double time) { + //Check if we still exist + if(obj is null || !obj.valid || obj.owner !is ai.empire) { + failed = true; + return false; + } + + uint ftlMode = F_Pass; + if(movement.ftl !is null) { + ftlMode = movement.ftl.tick(this, time); + if(ftlMode == F_Kill) + return false; + if(ftlMode == F_Done) + return true; + } + + //Check if we've arrived + if(target !is null) { + if(!target.valid) { + failed = true; + return false; + } + double targDist = target.radius + 45.0 + obj.radius; + if(target.isRegion) + targDist = target.radius * 0.86 + obj.radius; + if(target.position.distanceTo(obj.position) < targDist) { + completed = true; + return false; + } + } + else { + double targDist = obj.radius * 2.0; + if(obj.position.distanceTo(position) < targDist) { + completed = true; + return false; + } + } + + //Fail out if our order failed + if(ftlMode == F_Pass) { + if(!obj.hasOrders) { + failed = true; + return false; + } + } + + return true; + } +}; + +class Movement : AIComponent { + int nextMoveOrderId = 0; + array moveOrders; + + array oddities; + + FTL@ ftl; + + void create() { + @ftl = cast(ai.ftl); + } + + void save(SaveFile& file) { + file << nextMoveOrderId; + + uint cnt = moveOrders.length; + file << cnt; + for(uint i = 0; i < cnt; ++i) { + saveMoveOrder(file, moveOrders[i]); + moveOrders[i].save(this, file); + } + } + + void load(SaveFile& file) { + file >> nextMoveOrderId; + + uint cnt = 0; + file >> cnt; + for(uint i = 0; i < cnt; ++i) { + auto@ ord = loadMoveOrder(file); + if(ord !is null) { + ord.load(this, file); + if(ord.obj !is null) + moveOrders.insertLast(ord); + } + else { + MoveOrder().load(this, file); + } + } + } + + array loadIds; + MoveOrder@ loadMoveOrder(SaveFile& file) { + int id = -1; + file >> id; + bool failed = false, completed = false; + file >> failed; + file >> completed; + if(id == -1) { + return null; + } + else { + for(uint i = 0, cnt = loadIds.length; i < cnt; ++i) { + if(loadIds[i].id == id) + return loadIds[i]; + } + MoveOrder data; + data.id = id; + data.failed = failed; + data.completed = completed; + loadIds.insertLast(data); + return data; + } + } + + void saveMoveOrder(SaveFile& file, MoveOrder@ data) { + int id = -1; + bool failed = false, completed = false; + if(data !is null) { + id = data.id; + failed = data.failed; + completed = data.completed; + } + file << id; + file << failed; + file << completed; + } + + void postLoad(AI& ai) { + loadIds.length = 0; + getOddityGates(oddities); + } + + array path; + double getPathDistance(const vec3d& fromPosition, const vec3d& toPosition, double accel = 1.0) { + pathOddityGates(oddities, ai.empire, path, fromPosition, toPosition, accel); + return ::getPathDistance(fromPosition, toPosition, path); + } + + double eta(Object& obj, Object& toObject, uint priority = MP_Normal) { + return eta(obj, toObject.position, priority); + } + + double eta(Object& obj, const vec3d& position, uint priority = MP_Normal) { + //TODO: Use FTL + //TODO: Path through gates/wormholes + return newtonArrivalTime(obj.maxAcceleration, position - obj.position, obj.velocity); + } + + void order(MoveOrder& ord) { + if(ord.target !is null && ord.target is ord.obj.region) + return; + + bool madeOrder = false; + + if(ftl !is null) { + uint mode = ftl.order(ord); + if(mode == F_Kill || mode == F_Done) + return; + madeOrder = (mode == F_Continue); + } + + if(ord.target !is null) { + ord.obj.addGotoOrder(ord.target, append=madeOrder); + ord.position = ord.target.position; + } + else + ord.obj.addMoveOrder(ord.position, append=madeOrder); + } + + void add(MoveOrder& ord) { + for(uint i = 0, cnt = moveOrders.length; i < cnt; ++i) { + if(moveOrders[i].obj is ord.obj) { + moveOrders[i].failed = true; + moveOrders.removeAt(i); + --i; --cnt; + } + } + + moveOrders.insertLast(ord); + order(ord); + } + + MoveOrder@ move(Object& obj, Object& toObject, uint priority = MP_Normal, bool spread = false, bool nearOnly = false) { + if(toObject.isRegion) { + if(obj.region is toObject) + nearOnly = false; + else + nearOnly = true; + } + if(nearOnly) { + vec3d pos; + bool canNear = getNearPosition(obj, toObject, pos, spread); + if(canNear) + return move(obj, pos, priority); + } + + MoveOrder ord; + ord.id = nextMoveOrderId++; + @ord.obj = obj; + @ord.target = toObject; + ord.priority = priority; + + add(ord); + return ord; + } + + MoveOrder@ move(Object& obj, const vec3d& position, uint priority = MP_Normal, bool spread = false) { + MoveOrder ord; + ord.id = nextMoveOrderId++; + @ord.obj = obj; + ord.position = position; + ord.priority = priority; + + add(ord); + return ord; + } + + void tick(double time) override { + for(uint i = 0, cnt = moveOrders.length; i < cnt; ++i) { + if(!moveOrders[i].tick(ai, this, time)) { + moveOrders.removeAt(i); + --i; --cnt; + } + } + } + + void focusTick(double time) override { + //Update our gate navigation list + getOddityGates(oddities); + } +}; + +AIComponent@ createMovement() { + return Movement(); +} ADDED scripts/server/empire_ai/weasel/Orbitals.as Index: scripts/server/empire_ai/weasel/Orbitals.as ================================================================== --- scripts/server/empire_ai/weasel/Orbitals.as +++ scripts/server/empire_ai/weasel/Orbitals.as @@ -0,0 +1,261 @@ +import empire_ai.weasel.WeaselAI; +import empire_ai.weasel.Budget; +import empire_ai.weasel.Systems; + +from ai.orbitals import AIOrbitals, OrbitalAIHook, OrbitalUse; +import ai.consider; +import ai.construction; + +import orbitals; +import saving; + +final class OrbitalAI { + Object@ obj; + const OrbitalModule@ type; + double prevTick = 0; + Object@ around; + + void init(AI& ai, Orbitals& orbitals) { + if(obj.isOrbital) + @type = getOrbitalModule(cast(obj).coreModule); + } + + void save(Orbitals& orbitals, SaveFile& file) { + file << obj; + } + + void load(Orbitals& orbitals, SaveFile& file) { + file >> obj; + } + + void remove(AI& ai, Orbitals& orbitals) { + } + + void tick(AI& ai, Orbitals& orbitals, double time) { + //Deal with losing planet ownership + if(obj is null || !obj.valid || obj.owner !is ai.empire) { + orbitals.remove(this); + return; + } + + //Record what we're orbiting around + if(around !is null) { + if(!obj.isOrbitingAround(around)) + @around = obj.getOrbitingAround(); + } + else { + if(obj.hasOrbitCenter) + @around = obj.getOrbitingAround(); + } + } +}; + +class Orbitals : AIComponent, AIOrbitals { + Budget@ budget; + Systems@ systems; + + array orbitals; + uint orbIdx = 0; + + array genericBuilds; + + bool buildOrbitals = true; + + void create() { + @budget = cast(ai.budget); + @systems = cast(ai.systems); + + //Register specialized orbital types + for(uint i = 0, cnt = getOrbitalModuleCount(); i < cnt; ++i) { + auto@ type = getOrbitalModule(i); + for(uint n = 0, ncnt = type.ai.length; n < ncnt; ++n) { + auto@ hook = cast(type.ai[n]); + if(hook !is null) + hook.register(this, type); + } + } + } + + Empire@ get_empire() { + return ai.empire; + } + + Considerer@ get_consider() { + return cast(ai.consider); + } + + OrbitalAI@ getInSystem(const OrbitalModule@ module, Region@ reg) { + if(module is null) + return null; + for(uint i = 0, cnt = orbitals.length; i < cnt; ++i) { + if(orbitals[i].type is module) { + if(orbitals[i].obj.region is reg) + return orbitals[i]; + } + } + return null; + } + + bool haveInSystem(const OrbitalModule@ module, Region@ reg) { + if(module is null) + return false; + for(uint i = 0, cnt = orbitals.length; i < cnt; ++i) { + if(orbitals[i].type is module) { + if(orbitals[i].obj.region is reg) + return true; + } + } + return false; + } + + bool haveAround(const OrbitalModule@ module, Object@ around) { + if(module is null) + return false; + for(uint i = 0, cnt = orbitals.length; i < cnt; ++i) { + if(orbitals[i].type is module) { + if(orbitals[i].around is around) + return true; + } + } + return false; + } + + void registerUse(OrbitalUse use, const OrbitalModule& type) { + switch(use) { + case OU_Shipyard: + @ai.defs.Shipyard = type; + break; + case OU_TradeOutpost: + @ai.defs.TradeOutpost = type; + break; + case OU_TradeStation: + @ai.defs.TradeStation = type; + break; + } + } + + void save(SaveFile& file) { + uint cnt = orbitals.length; + file << cnt; + for(uint i = 0; i < cnt; ++i) { + auto@ data = orbitals[i]; + saveAI(file, data); + data.save(this, file); + } + } + + void load(SaveFile& file) { + uint cnt = 0; + file >> cnt; + for(uint i = 0; i < cnt; ++i) { + auto@ data = loadAI(file); + if(data !is null) + data.load(this, file); + else + OrbitalAI().load(this, file); + } + } + + OrbitalAI@ loadAI(SaveFile& file) { + Object@ obj; + file >> obj; + + if(obj is null) + return null; + + OrbitalAI@ data = getAI(obj); + if(data is null) { + @data = OrbitalAI(); + @data.obj = obj; + data.prevTick = gameTime; + orbitals.insertLast(data); + data.init(ai, this); + } + return data; + } + + void saveAI(SaveFile& file, OrbitalAI@ ai) { + Object@ obj; + if(ai !is null) + @obj = ai.obj; + file << obj; + } + + void start() { + checkForOrbitals(); + } + + void checkForOrbitals() { + auto@ data = ai.empire.getOrbitals(); + Object@ obj; + while(receive(data, obj)) { + if(obj !is null) + register(obj); + } + } + + bool isBuilding(const OrbitalModule& type) { + for(uint i = 0, cnt = genericBuilds.length; i < cnt; ++i) { + if(genericBuilds[i].module is type) + return true; + } + return false; + } + + void tick(double time) { + double curTime = gameTime; + + if(orbitals.length != 0) { + orbIdx = (orbIdx+1) % orbitals.length; + + auto@ data = orbitals[orbIdx]; + data.tick(ai, this, curTime - data.prevTick); + data.prevTick = curTime; + } + } + + uint prevCount = 0; + double checkTimer = 0; + void focusTick(double time) override { + //Check for any newly obtained planets + uint curCount = ai.empire.orbitalCount; + checkTimer += time; + if(curCount != prevCount || checkTimer > 60.0) { + checkForOrbitals(); + prevCount = curCount; + checkTimer = 0; + } + + //Deal with building AI hints + + } + + OrbitalAI@ getAI(Object& obj) { + for(uint i = 0, cnt = orbitals.length; i < cnt; ++i) { + if(orbitals[i].obj is obj) + return orbitals[i]; + } + return null; + } + + OrbitalAI@ register(Object& obj) { + OrbitalAI@ data = getAI(obj); + if(data is null) { + @data = OrbitalAI(); + @data.obj = obj; + data.prevTick = gameTime; + orbitals.insertLast(data); + data.init(ai, this); + } + return data; + } + + void remove(OrbitalAI@ data) { + data.remove(ai, this); + orbitals.remove(data); + } +}; + +AIComponent@ createOrbitals() { + return Orbitals(); +} ADDED scripts/server/empire_ai/weasel/Planets.as Index: scripts/server/empire_ai/weasel/Planets.as ================================================================== --- scripts/server/empire_ai/weasel/Planets.as +++ scripts/server/empire_ai/weasel/Planets.as @@ -0,0 +1,1000 @@ +import empire_ai.weasel.WeaselAI; + +import empire_ai.weasel.Events; +import empire_ai.weasel.Resources; +import empire_ai.weasel.Budget; +import empire_ai.weasel.Systems; + +import ai.construction; +import ai.events; + +import planets.PlanetSurface; + +import void relationRecordLost(AI& ai, Empire& emp, Object@ obj) from "empire_ai.weasel.Relations"; + +from constructions import ConstructionType, getConstructionTypeCount, getConstructionType; +from ai.constructions import AIConstructions, ConstructionAIHook, ConstructionUse; + +import ai.consider; + +import buildings; +import saving; + +final class BuildingRequest : IBuildingConstruction { + protected int _id = -1; + PlanetAI@ plAI; + AllocateBudget@ alloc; + const BuildingType@ type; + double expires = INFINITY; + bool built = false; + bool canceled = false; + bool scatter = false; + vec2i builtAt; + + BuildingRequest(Budget& budget, const BuildingType@ type, double priority, uint moneyType) { + @this.type = type; + @alloc = budget.allocate(moneyType, type.buildCostEst, type.maintainCostEst, priority=priority); + } + + BuildingRequest() { + } + + int id { + get const { return _id; } + set { _id = value; } + } + + bool get_started() const { return built; } + + bool completed { + get const { return getProgress() >= 1.0; } + set { } + } + + const BuildingType@ get_building() const { return type; } + + void save(Planets& planets, SaveFile& file) { + planets.saveAI(file, plAI); + planets.budget.saveAlloc(file, alloc); + file.writeIdentifier(SI_Building, type.id); + file << expires; + file << built; + file << canceled; + file << builtAt; + file << scatter; + } + + void load(Planets& planets, SaveFile& file) { + @plAI = planets.loadAI(file); + @alloc = planets.budget.loadAlloc(file); + @type = getBuildingType(file.readIdentifier(SI_Building)); + file >> expires; + file >> built; + file >> canceled; + file >> builtAt; + if(file >= SV_0153) + file >> scatter; + } + + double cachedProgress = 0.0; + double nextProgressCache = 0.0; + double getProgress() { + if(!built) + return 0.0; + if(gameTime < nextProgressCache) + return cachedProgress; + + cachedProgress = plAI.obj.getBuildingProgressAt(builtAt.x, builtAt.y); + if(cachedProgress > 0.95) + nextProgressCache = gameTime + 1.0; + else if(cachedProgress < 0.5) + nextProgressCache = gameTime + 30.0; + else + nextProgressCache = gameTime + 10.0; + + return cachedProgress; + } + + bool tick(AI& ai, Planets& planets, double time) { + if(expires < gameTime) { + if(planets.log) + ai.print(type.name+" build request expired", plAI.obj); + canceled = true; + return false; + } + + if(alloc is null || alloc.allocated) { + builtAt = plAI.buildBuilding(ai, planets, type, scatter=scatter); + if(builtAt == vec2i(-1,-1)) { + planets.budget.remove(alloc); + canceled = true; + } + else + built = true; + return false; + } + return true; + } +}; + +final class ConstructionRequest : IGenericConstruction { + protected int _id = -1; + PlanetAI@ plAI; + AllocateBudget@ alloc; + const ConstructionType@ type; + double expires = INFINITY; + bool built = false; + bool canceled = false; + vec2i builtAt; + + ConstructionRequest(Budget& budget, Object@ buildAt, const ConstructionType@ type, double priority, uint moneyType) { + @this.type = type; + @alloc = budget.allocate(moneyType, type.getBuildCost(buildAt), type.getMaintainCost(buildAt), priority=priority); + } + + ConstructionRequest() { + } + + int id { + get const { return _id; } + set { _id = value; } + } + + bool get_started() const { return built; } + + bool completed { + get const { + double progress = getProgress(); + return progress == -1.0 || progress >= 1.0; + } + set { } + } + + const ConstructionType@ get_construction() const { return type; } + + void save(Planets& planets, SaveFile& file) { + planets.saveAI(file, plAI); + planets.budget.saveAlloc(file, alloc); + file.writeIdentifier(SI_ConstructionType, type.id); + file << expires; + file << built; + file << canceled; + file << builtAt; + } + + void load(Planets& planets, SaveFile& file) { + @plAI = planets.loadAI(file); + @alloc = planets.budget.loadAlloc(file); + @type = getConstructionType(file.readIdentifier(SI_ConstructionType)); + file >> expires; + file >> built; + file >> canceled; + file >> builtAt; + } + + double cachedProgress = 0.0; + double nextProgressCache = 0.0; + double getProgress() { + if(!built) + return 0.0; + if(gameTime < nextProgressCache) + return cachedProgress; + + cachedProgress = plAI.obj.get_constructionProgress(); + if(cachedProgress > 0.95) + nextProgressCache = gameTime + 1.0; + else if(cachedProgress < 0.5) + nextProgressCache = gameTime + 30.0; + else + nextProgressCache = gameTime + 10.0; + + return cachedProgress; + } + + bool tick(AI& ai, Planets& planets, double time) { + if(expires < gameTime) { + if(planets.log) + ai.print(type.name+" construction request expired", plAI.obj); + canceled = true; + return false; + } + + if(alloc is null || alloc.allocated) { + if(!plAI.buildConstruction(ai, planets, type)) { + planets.budget.remove(alloc); + canceled = true; + + } + else + built = true; + return false; + } + return true; + } +}; + +final class PlanetAI { + Planet@ obj; + + int targetLevel = 0; + int requestedLevel = 0; + double prevTick = 0; + + array@ resources; + ImportData@ claimedChain; + + void init(AI& ai, Planets& planets) { + @resources = planets.resources.availableResource(obj); + planets.events.notifyPlanetAdded(this, EventArgs()); + } + + void save(Planets& planets, SaveFile& file) { + file << obj; + file << targetLevel; + file << requestedLevel; + file << prevTick; + + uint cnt = 0; + if(resources !is null) + cnt = resources.length; + file << cnt; + for(uint i = 0; i < cnt; ++i) + planets.resources.saveExport(file, resources[i]); + planets.resources.saveImport(file, claimedChain); + } + + void load(Planets& planets, SaveFile& file) { + file >> obj; + file >> targetLevel; + file >> requestedLevel; + file >> prevTick; + uint cnt = 0; + file >> cnt; + @resources = array(); + for(uint i = 0; i < cnt; ++i) { + auto@ data = planets.resources.loadExport(file); + if(data !is null) + resources.insertLast(data); + } + @claimedChain = planets.resources.loadImport(file); + } + + void remove(AI& ai, Planets& planets) { + if(claimedChain !is null) { + claimedChain.claimedFor = false; + @claimedChain = null; + } + @resources = null; + planets.events.notifyPlanetRemoved(this, EventArgs()); + } + + void tick(AI& ai, Planets& planets, double time) { + //Deal with losing planet ownership + if(obj is null || !obj.valid || obj.owner !is ai.empire) { + if(obj.owner !is ai.empire) + relationRecordLost(ai, obj.owner, obj); + planets.remove(this); + return; + } + + //Handle when the planet's native resources change + if(obj.nativeResourceCount != resources.length || (resources.length != 0 && obj.primaryResourceId != resources[0].resourceId)) + planets.updateResourceList(obj, resources); + + //Level up resources if we need them + if(resources.length != 0 && claimedChain is null) { + int resLevel = resources[0].resource.level; + if(resLevel > 0 && !resources[0].resource.exportable) + resLevel += 1; + if(targetLevel < resLevel) { + //See if we need it for anything first + @claimedChain = planets.resources.findUnclaimed(resources[0]); + if(claimedChain !is null) + claimedChain.claimedFor = true; + + //Chain the levelup before what needs it + planets.requestLevel(this, resLevel, before=claimedChain); + } + } + + //Request imports if the planet needs to level up + if(targetLevel > requestedLevel) { + int nextLevel = min(targetLevel, min(obj.resourceLevel, requestedLevel)+1); + if(nextLevel != requestedLevel) { + planets.resources.organizeImports(obj, nextLevel); + requestedLevel = nextLevel; + } + } + else if(targetLevel < requestedLevel) { + planets.resources.organizeImports(obj, targetLevel); + requestedLevel = targetLevel; + } + } + + double get_colonizeWeight() { + if(obj.isColonizing) + return 0.0; + if(obj.level == 0) + return 0.0; + if(!obj.canSafelyColonize) + return 0.0; + double w = 1.0; + double pop = obj.population; + double maxPop = obj.maxPopulation; + if(pop < maxPop-0.1) { + if(obj.resourceLevel > 1 && pop/maxPop < 0.9) + return 0.0; + w *= 0.01 * (pop / maxPop); + } + return w; + } + + vec2i buildBuilding(AI& ai, Planets& planets, const BuildingType@ type, bool scatter = true) { + if(type is null || !type.canBuildOn(obj)) + return vec2i(-1,-1); + + if(planets.log) + ai.print("Attempt to construct "+type.name, obj); + + PlanetSurface@ surface = planets.surface; + receive(obj.getPlanetSurface(), surface); + + //Find the best place to build this building + int bestPenalty = INT_MAX; + int possibs = 0; + vec2i best; + vec2i center = vec2i(type.getCenter()); + + for(int x = 0, w = surface.size.x; x < w; ++x) { + for(int y = 0, h = surface.size.y; y < h; ++y) { + vec2i pos(x, y); + + bool valid = true; + int penalty = 0; + + for(int xoff = 0; xoff < int(type.size.x); ++xoff) { + for(int yoff = 0; yoff < int(type.size.y); ++yoff) { + vec2i rpos = pos - center + vec2i(xoff, yoff); + + if(rpos.x < 0 || rpos.y < 0 || rpos.x >= w || rpos.y >= h) { + valid = false; + break; + } + + auto@ biome = surface.getBiome(rpos.x, rpos.y); + if(biome is null || !biome.buildable) { + valid = false; + break; + } + + uint flags = surface.getFlags(rpos.x, rpos.y); + if(flags & SuF_Usable == 0) { + bool affinity = false; + if(type.buildAffinities.length != 0) { + for(uint i = 0, cnt = type.buildAffinities.length; i < cnt; ++i) { + if(biome is type.buildAffinities[i].biome) { + affinity = true; + break; + } + } + } + if(!affinity && type.tileBuildCost > 0) { + penalty += 1; + + if(biome.buildCost > 1.0) + penalty += ceil((biome.buildCost - 1.0) / 0.1); + } + affinity = false; + if(type.maintainAffinities.length != 0) { + for(uint i = 0, cnt = type.maintainAffinities.length; i < cnt; ++i) { + if(biome is type.maintainAffinities[i].biome) { + affinity = true; + break; + } + } + } + if(!affinity && type.tileMaintainCost > 0) + penalty += 2; + } + + auto@ bld = surface.getBuilding(rpos.x, rpos.y); + if(bld !is null) { + if(bld.type.civilian) { + penalty += 2; + } + else { + valid = false; + break; + } + } + } + if(!valid) + break; + } + + if(valid) { + if(penalty < bestPenalty) { + possibs = 1; + bestPenalty = penalty; + best = pos; + } + else if(penalty == bestPenalty && scatter) { + possibs += 1; + if(randomd() < 1.0 / double(possibs)) + best = pos; + } + } + } + } + + if(bestPenalty != INT_MAX) { + if(planets.log) + ai.print("Construct "+type.name+" at "+best+" with penalty "+bestPenalty, obj); + obj.buildBuilding(type.id, best); + return best; + } + + if(planets.log) + ai.print("Could not find place to construct "+type.name, obj); + return vec2i(-1,-1); + } + + bool buildConstruction(AI& ai, Planets& planets, const ConstructionType@ type) { + if(type is null || !type.canBuild(obj)) + return false; + + if(planets.log) + ai.print("Construct "+type.name); + obj.buildConstruction(type.id); + + return true; + } +} + +final class PotentialSource { + Planet@ pl; + double weight = 0; +}; + +final class AsteroidData { + Asteroid@ asteroid; + array@ resources; + + void save(Planets& planets, SaveFile& file) { + file << asteroid; + + uint cnt = 0; + if(resources !is null) + cnt = resources.length; + + file << cnt; + for(uint i = 0; i < cnt; ++i) + planets.resources.saveExport(file, resources[i]); + } + + void load(Planets& planets, SaveFile& file) { + file >> asteroid; + + uint cnt = 0; + file >> cnt; + if(cnt != 0) + @resources = array(); + for(uint i = 0; i < cnt; ++i) { + auto@ res = planets.resources.loadExport(file); + if(res !is null) + resources.insertLast(res); + } + } + + bool tick(AI& ai, Planets& planets) { + if(asteroid is null || !asteroid.valid || asteroid.owner !is ai.empire) { + planets.resources.killImportsTo(asteroid); + planets.resources.killResourcesFrom(asteroid); + return false; + } + if(resources is null) { + @resources = planets.resources.availableResource(asteroid); + } + else { + if(asteroid.nativeResourceCount != resources.length || (resources.length != 0 && asteroid.primaryResourceId != resources[0].resourceId)) + planets.updateResourceList(asteroid, resources); + } + return true; + } +}; + +class Planets : AIComponent, AIConstructions { + Events@ events; + Resources@ resources; + Budget@ budget; + Systems@ systems; + + PlanetSurface surface; + + array ownedAsteroids; + array planets; + array bumped; + uint planetIdx = 0; + + + array building; + int nextBuildingRequestId = 0; + array constructionRequests; + int nextConstructionRequestId = 0; + + void create() { + @events = cast(ai.events); + @resources = cast(ai.resources); + @budget = cast(ai.budget); + @systems = cast(ai.systems); + + //Register specialized construction types + for(uint i = 0, cnt = getConstructionTypeCount(); i < cnt; ++i) { + auto@ type = getConstructionType(i); + for(uint n = 0, ncnt = type.ai.length; n < ncnt; ++n) { + auto@ hook = cast(type.ai[n]); + if(hook !is null) + hook.register(this, type); + } + } + } + + Empire@ get_empire() { + return ai.empire; + } + + Considerer@ get_consider() { + return cast(ai.consider); + } + + void registerUse(ConstructionUse use, const ConstructionType& type) { + switch(use) { + case CU_MoonBase: + @ai.defs.MoonBase = type; + break; + } + } + + void save(SaveFile& file) { + file << nextBuildingRequestId; + file << nextConstructionRequestId; + + uint cnt = planets.length; + file << cnt; + for(uint i = 0; i < cnt; ++i) { + auto@ plAI = planets[i]; + saveAI(file, plAI); + plAI.save(this, file); + } + + cnt = building.length; + file << cnt; + for(uint i = 0; i < cnt; ++i) { + saveBuildingRequest(file, building[i]); + building[i].save(this, file); + } + + cnt = constructionRequests.length; + file << cnt; + for(uint i = 0; i < cnt; ++i) { + saveConstructionRequest(file, constructionRequests[i]); + constructionRequests[i].save(this, file); + } + + cnt = ownedAsteroids.length; + file << cnt; + for(uint i = 0; i < cnt; ++i) + ownedAsteroids[i].save(this, file); + } + + void load(SaveFile& file) { + file >> nextBuildingRequestId; + file >> nextConstructionRequestId; + + uint cnt = 0; + file >> cnt; + for(uint i = 0; i < cnt; ++i) { + auto@ plAI = loadAI(file); + if(plAI !is null) + plAI.load(this, file); + else + PlanetAI().load(this, file); + } + + file >> cnt; + for(uint i = 0; i < cnt; ++i) { + auto@ req = loadBuildingRequest(file); + if(req !is null) { + req.load(this, file); + building.insertLast(req); + } + } + + file >> cnt; + for(uint i = 0; i < cnt; ++i) { + auto@ req = loadConstructionRequest(file); + if (req !is null) { + req.load(this, file); + constructionRequests.insertLast(req); + } + } + + file >> cnt; + for(uint i = 0; i < cnt; ++i) { + AsteroidData data; + data.load(this, file); + if(data.asteroid !is null) + ownedAsteroids.insertLast(data); + } + } + + PlanetAI@ loadAI(SaveFile& file) { + Planet@ obj; + file >> obj; + + if(obj is null) + return null; + + PlanetAI@ plAI = getAI(obj); + if(plAI is null) { + @plAI = PlanetAI(); + @plAI.obj = obj; + plAI.prevTick = gameTime; + planets.insertLast(plAI); + } + return plAI; + } + + void saveAI(SaveFile& file, PlanetAI@ ai) { + Planet@ pl; + if(ai !is null) + @pl = ai.obj; + file << pl; + } + + array buildingLoadIds; + BuildingRequest@ loadBuildingRequest(int id) { + if(id == -1) + return null; + for(uint i = 0, cnt = buildingLoadIds.length; i < cnt; ++i) { + if(buildingLoadIds[i].id == id) + return buildingLoadIds[i]; + } + BuildingRequest data; + data.id = id; + buildingLoadIds.insertLast(data); + return data; + } + BuildingRequest@ loadBuildingRequest(SaveFile& file) { + int id = -1; + file >> id; + if(id == -1) + return null; + else + return loadBuildingRequest(id); + } + void saveBuildingRequest(SaveFile& file, BuildingRequest@ data) { + int id = -1; + if(data !is null) + id = data.id; + file << id; + } + array constructionLoadIds; + ConstructionRequest@ loadConstructionRequest(int id) { + if(id == -1) + return null; + for(uint i = 0, cnt = constructionLoadIds.length; i < cnt; ++i) { + if(constructionLoadIds[i].id == id) + return constructionLoadIds[i]; + } + ConstructionRequest data; + data.id = id; + constructionLoadIds.insertLast(data); + return data; + } + ConstructionRequest@ loadConstructionRequest(SaveFile& file) { + int id = -1; + file >> id; + if(id == -1) + return null; + else + return loadConstructionRequest(id); + } + void saveConstructionRequest(SaveFile& file, ConstructionRequest@ data) { + int id = -1; + if(data !is null) + id = data.id; + file << id; + } + void postLoad(AI& ai) { + buildingLoadIds.length = 0; + constructionLoadIds.length= 0; + } + + void start() { + checkForPlanets(); + } + + void checkForPlanets() { + auto@ data = ai.empire.getPlanets(); + Object@ obj; + while(receive(data, obj)) { + Planet@ pl = cast(obj); + if(pl !is null) + register(cast(obj)); + } + } + + uint roidIdx = 0; + void tick(double time) { + double curTime = gameTime; + + if(planets.length != 0) { + planetIdx = (planetIdx+1) % planets.length; + + auto@ plAI = planets[planetIdx]; + plAI.tick(ai, this, curTime - plAI.prevTick); + plAI.prevTick = curTime; + } + + for(int i = bumped.length-1; i >= 0; --i) { + auto@ plAI = bumped[i]; + double tickTime = curTime - plAI.prevTick; + if(tickTime != 0) { + plAI.tick(ai, this, tickTime); + plAI.prevTick = curTime; + } + } + bumped.length = 0; + + if(ownedAsteroids.length != 0) { + roidIdx = (roidIdx+1) % ownedAsteroids.length; + if(!ownedAsteroids[roidIdx].tick(ai, this)) + ownedAsteroids.removeAt(roidIdx); + } + + //Construct any buildings we are waiting on + for(uint i = 0, cnt = building.length; i < cnt; ++i) { + if(!building[i].tick(ai, this, time)) { + building.removeAt(i); + --i; --cnt; + break; + } + } + + //Construct any constructions we are waiting on + for(uint i = 0, cnt = constructionRequests.length; i < cnt; ++i) { + if(!constructionRequests[i].tick(ai, this, time)) { + constructionRequests.removeAt(i); + --i; --cnt; + break; + } + } + } + + uint prevCount = 0; + double checkTimer = 0; + uint sysIdx = 0, ownedIdx = 0; + void focusTick(double time) override { + //Check for any newly obtained planets + uint curCount = ai.empire.planetCount; + checkTimer += time; + if(curCount != prevCount || checkTimer > 60.0) { + checkForPlanets(); + prevCount = curCount; + checkTimer = 0; + } + + //Find any asteroids we've gained + if(systems.all.length != 0) { + sysIdx = (sysIdx+1) % systems.all.length; + auto@ sys = systems.all[sysIdx]; + for(uint i = 0, cnt = sys.asteroids.length; i < cnt; ++i) + register(sys.asteroids[i]); + } + if(systems.owned.length != 0) { + ownedIdx = (ownedIdx+1) % systems.owned.length; + auto@ sys = systems.owned[ownedIdx]; + for(uint i = 0, cnt = sys.asteroids.length; i < cnt; ++i) + register(sys.asteroids[i]); + } + } + + void bump(Planet@ pl) { + if(pl !is null) + bump(getAI(pl)); + } + + void bump(PlanetAI@ plAI) { + if(plAI !is null) + bumped.insertLast(plAI); + } + + PlanetAI@ getAI(Planet& obj) { + for(uint i = 0, cnt = planets.length; i < cnt; ++i) { + if(planets[i].obj is obj) + return planets[i]; + } + return null; + } + + PlanetAI@ register(Planet& obj) { + PlanetAI@ plAI = getAI(obj); + if(plAI is null) { + @plAI = PlanetAI(); + @plAI.obj = obj; + plAI.prevTick = gameTime; + planets.insertLast(plAI); + plAI.init(ai, this); + } + return plAI; + } + + AsteroidData@ register(Asteroid@ obj) { + if(obj is null || !obj.valid || obj.owner !is ai.empire) + return null; + for(uint i = 0, cnt = ownedAsteroids.length; i < cnt; ++i) { + if(ownedAsteroids[i].asteroid is obj) + return ownedAsteroids[i]; + } + + AsteroidData data; + @data.asteroid = obj; + ownedAsteroids.insertLast(data); + + if(log) + ai.print("Detected asteroid: "+obj.name, obj.region); + + return data; + } + + void remove(PlanetAI@ plAI) { + resources.killImportsTo(plAI.obj); + resources.killResourcesFrom(plAI.obj); + plAI.remove(ai, this); + planets.remove(plAI); + bumped.remove(plAI); + } + + void requestLevel(PlanetAI@ plAI, int toLevel, ImportData@ before = null) { + if(plAI is null) + return; + plAI.targetLevel = toLevel; + if(before !is null) { + for(int lv = max(plAI.requestedLevel, 1); lv <= toLevel; ++lv) + resources.organizeImports(plAI.obj, lv, before); + plAI.requestedLevel = toLevel; + } + else { + bump(plAI); + } + } + + BuildingRequest@ requestBuilding(PlanetAI@ plAI, const BuildingType@ type, double priority = 1.0, double expire = INFINITY, bool scatter = true, uint moneyType = BT_Development) { + if(plAI is null) + return null; + + if(log) + ai.print("Requested building of type "+type.name, plAI.obj); + + BuildingRequest req(budget, type, priority, moneyType); + req.scatter = scatter; + req.id = nextBuildingRequestId++; + req.expires = gameTime + expire; + @req.plAI = plAI; + + building.insertLast(req); + return req; + } + + ConstructionRequest@ requestConstruction(PlanetAI@ plAI, Object@ buildAt, const ConstructionType@ type, double priority = 1.0, double expire = INFINITY, uint moneyType = BT_Development) { + if(plAI is null) + return null; + + if(log) + ai.print("Requested construction of type "+type.name, plAI.obj); + + ConstructionRequest req(budget, buildAt, type, priority, moneyType); + req.id = nextConstructionRequestId++; + req.expires = gameTime + expire; + @req.plAI = plAI; + + constructionRequests.insertLast(req); + return req; + } + + bool isBuilding(Planet@ planet, const BuildingType@ type) { + for(uint i = 0, cnt = building.length; i < cnt; ++i) { + if(building[i].type is type && building[i].plAI.obj is planet) + return true; + } + return false; + } + + bool isBuilding(Planet@ planet, const ConstructionType@ type) { + for(uint i = 0, cnt = constructionRequests.length; i < cnt; ++i) { + if(constructionRequests[i].type is type && constructionRequests[i].plAI.obj is planet) + return true; + } + return false; + } + + void getColonizeSources(array& sources) { + sources.length = 0; + for(uint i = 0, cnt = planets.length; i < cnt; ++i) { + auto@ plAI = planets[i]; + if(!plAI.obj.valid) + continue; + + double w = plAI.colonizeWeight; + if(w == 0) + continue; + if(plAI.obj.owner !is ai.empire) + continue; + + PotentialSource src; + @src.pl = planets[i].obj; + src.weight = w; + sources.insertLast(src); + } + } + + array newResources; + array removedResources; + array checkResources; + void updateResourceList(Object@ obj, array& resList) { + newResources.length = 0; + removedResources = resList; + + checkResources.syncFrom(obj.getNativeResources()); + + uint nativeCnt = checkResources.length; + for(uint i = 0; i < nativeCnt; ++i) { + int id = checkResources[i].id; + + bool found = false; + for(uint n = 0, ncnt = removedResources.length; n < ncnt; ++n) { + if(removedResources[n].resourceId == id) { + removedResources.removeAt(n); + found = true; + break; + } + } + + if(!found) { + auto@ type = checkResources[i].type; + auto@ res = resources.availableResource(obj, type, id); + + if(i == 0) + resList.insertAt(0, res); + else + resList.insertLast(res); + newResources.insertLast(res); + } + else if(i == 0 && resList.length > 1 && resList[0].resourceId != id) { + for(uint n = 0, ncnt = resList.length; n < ncnt; ++n) { + if(resList[n].resourceId == id) { + auto@ res = resList[n]; + resList.removeAt(n); + resList.insertAt(0, res); + break; + } + } + } + } + + //Get rid of resources we no longer have + for(uint i = 0, cnt = removedResources.length; i < cnt; ++i) { + resources.removeResource(removedResources[i]); + resList.remove(removedResources[i]); + } + + //Tell the resources component to try to immediately use the new resources + for(uint i = 0, cnt = newResources.length; i < cnt; ++i) + resources.checkReplaceCurrent(newResources[i]); + } +}; + +AIComponent@ createPlanets() { + return Planets(); +} ADDED scripts/server/empire_ai/weasel/Relations.as Index: scripts/server/empire_ai/weasel/Relations.as ================================================================== --- scripts/server/empire_ai/weasel/Relations.as +++ scripts/server/empire_ai/weasel/Relations.as @@ -0,0 +1,869 @@ +// Relations +// --------- +// Manages the relationships we have with other empires, including treaties, hatred, and wars. +// + +import empire_ai.weasel.WeaselAI; + +import empire_ai.weasel.Intelligence; +import empire_ai.weasel.Fleets; +import empire_ai.weasel.Systems; +import empire_ai.weasel.Planets; + +import warandpeace; +import influence; +from influence_global import activeTreaties, influenceLock, joinTreaty, leaveTreaty, declineTreaty, Treaty, sendPeaceOffer, createTreaty, offerSurrender, demandSurrender, leaveTreatiesWith; + +enum HateType { + HT_SystemPresence, + HT_FleetPresence, + HT_COUNT +}; + +class Hate { + uint type; + double amount = 0.0; + Object@ obj; + SystemAI@ sys; + + void save(Relations& relations, SaveFile& file) { + file << type; + file << amount; + file << obj; + relations.systems.saveAI(file, sys); + } + + void load(Relations& relations, SaveFile& file) { + file >> type; + file >> amount; + file >> obj; + @sys = relations.systems.loadAI(file); + } + + bool get_valid() { + if(type == HT_SystemPresence) + return sys !is null; + if(type == HT_FleetPresence) + return obj !is null && sys !is null; + return true; + } + + bool update(AI& ai, Relations& relations, Relation& rel, double time) { + if(type == HT_SystemPresence) { + amount = 0.25; + if(sys.seenPresent & rel.empire.mask == 0) + return false; + if(sys.seenPresent & ai.empire.mask == 0) + return false; + } + else if(type == HT_FleetPresence) { + if(!obj.valid || obj.owner !is rel.empire) + return false; + if(sys.seenPresent & ai.empire.mask == 0) + return false; + if(obj.region !is sys.obj) + return false; + if(obj.getFleetStrength() < 1000.0) + amount = 0.1; + else + amount = 0.5; + } + + rel.hate += amount * time; + return true; + } + + string dump() { + switch(type) { + case HT_SystemPresence: + return "system presence in "+sys.obj.name; + case HT_FleetPresence: + return "fleet presence "+obj.name+" in "+sys.obj.name; + } + return "unknown"; + } +}; + +final class Relation { + Empire@ empire; + + //Whether we've met this empire + bool contacted = false; + + //Whether we're currently at war + bool atWar = false; + + //Last time we tried to make peace + double lastPeaceTry = 0; + + //Whether this is our war of aggression + bool aggressive = false; + + //Whether this is our ally + bool allied = false; + + //Our relationship data + double hate = 0.0; + array hates; + + //Masks + uint borderedTo = 0; + uint alliedTo = 0; + + //Whether we consider this empire a threat to us + bool isThreat = false; + + //How much we would value having this empire as an ally + double allyValue = 0.0; + //How much we think we can beat this empire and all its allies + double defeatable = 0.0; + //Relative strength of this empire to us in a vacuum + double relStrength = 0.0; + + //How much we've lost to them in this recent war + double warLost = 0.0; + //How much we've taken from them in this recent war + double warTaken = 0.0; + + void save(Relations& relations, SaveFile& file) { + file << contacted; + file << atWar; + file << aggressive; + file << allied; + + file << hate; + uint cnt = hates.length; + file << cnt; + for(uint i = 0; i < cnt; ++i) + hates[i].save(relations, file); + + file << borderedTo; + file << alliedTo; + file << isThreat; + file << allyValue; + file << defeatable; + file << relStrength; + file << warTaken; + file << warLost; + file << lastPeaceTry; + } + + void load(Relations& relations, SaveFile& file) { + file >> contacted; + file >> atWar; + file >> aggressive; + file >> allied; + + file >> hate; + uint cnt = 0; + file >> cnt; + for(uint i = 0; i < cnt; ++i) { + Hate ht; + ht.load(relations, file); + if(ht.valid) + hates.insertLast(ht); + } + + file >> borderedTo; + file >> alliedTo; + file >> isThreat; + file >> allyValue; + file >> defeatable; + file >> relStrength; + file >> warTaken; + file >> warLost; + file >> lastPeaceTry; + } + + void trackSystem(AI& ai, Relations& relations, SystemAI@ sys) { + for(uint i = 0, cnt = hates.length; i < cnt; ++i) { + auto@ ht = hates[i]; + if(ht.type != HT_SystemPresence) + continue; + if(ht.sys is sys) + return; + } + + Hate ht; + ht.type = HT_SystemPresence; + @ht.sys = sys; + hates.insertLast(ht); + + if(relations.log) + ai.print("Gain hate of "+empire.name+": "+ht.dump()); + } + + void trackFleet(AI& ai, Relations& relations, FleetIntel@ intel, SystemAI@ sys) { + for(uint i = 0, cnt = hates.length; i < cnt; ++i) { + auto@ ht = hates[i]; + if(ht.type != HT_FleetPresence) + continue; + if(ht.obj is intel.obj && ht.sys is sys) + return; + } + + Hate ht; + ht.type = HT_FleetPresence; + @ht.sys = sys; + @ht.obj = intel.obj; + hates.insertLast(ht); + + if(relations.log) + ai.print("Gain hate of "+empire.name+": "+ht.dump()); + } + + void tick(AI& ai, Relations& relations, double time) { + if(!contacted) { + if(ai.empire.ContactMask & empire.mask != 0) + contacted = true; + } + + bool curWar = ai.empire.isHostile(empire); + if(curWar != atWar) + atWar = curWar; + if(!atWar) { + aggressive = false; + warLost = 0.0; + warTaken = 0.0; + lastPeaceTry = 0.0; + } + + borderedTo = relations.intelligence.get(empire).borderedTo; + alliedTo = empire.mask | empire.mutualDefenseMask | empire.ForcedPeaceMask.value; + + defeatable = relations.intelligence.defeatability(alliedTo, ai.mask | ai.allyMask); + relStrength = relations.intelligence.defeatability(ai.mask, empire.mask); + isThreat = defeatable < 0.8 && (borderedTo & ai.empire.mask) != 0; + + //Check how valuable of an ally this empire would make + allyValue = 1.0; + for(uint i = 0, cnt = relations.relations.length; i < cnt; ++i) { + auto@ other = relations.relations[i]; + if(other is null || other is this || other.empire is null) + continue; + if(other.borderedTo & empire.mask == 0) + continue; + if(alliedTo & empire.mask != 0) + continue; + + if(other.atWar) + allyValue *= 3.0; + else if(other.isThreat) + allyValue *= 1.5; + } + + //Become aggressive here if we're aggressive against one of its allies + if(atWar && !aggressive) { + for(uint i = 0, cnt = relations.relations.length; i < cnt; ++i) { + auto@ other = relations.relations[i]; + if(other is null || other is this || other.empire is null) + continue; + if(other.aggressive && this.alliedTo & other.empire.mask != 0) { + aggressive = true; + break; + } + } + } + + //Update our hatred of them + for(uint i = 0, cnt = hates.length; i < cnt; ++i) { + if(!hates[i].update(ai, relations, this, time)) { + if(relations.log) + ai.print("Hate with "+empire.name+" expired: "+hates[i].dump()); + hates.removeAt(i); + --i; --cnt; + } + } + + hate *= pow(ai.behavior.hateDecayRate, time / 60.0); + if(ai.behavior.biased && !empire.isAI) + hate += 1.0; + + if(ai.behavior.forbidDiplomacy) return; + + //If we really really hate them, declare war + if(!atWar || !aggressive) { + double reqHate = 100.0; + if(defeatable < 1.0) + reqHate *= sqr(1.0 / defeatable); + reqHate *= pow(2.0, relations.warCount()); + + if(hate > reqHate && (!ai.behavior.passive || atWar) && defeatable >= ai.behavior.hatredWarOverkill) { + //Make sure our other requirements for war are met + if(relations.fleets.haveCombatReadyFleets()) { + if(canDeclareWar(ai)) { + if(relations.log) + ai.print("Declaring hatred war on "+empire.name+": "+hate+" / "+reqHate); + if(atWar) + aggressive = true; + else + relations.declareWar(empire, aggressive=true); + } + } + } + } + } + + bool isAllied(AI& ai) { + return alliedTo & ai.empire.mask != 0; + } + + bool canDeclareWar(AI& ai) { + if(empire.SubjugatedBy !is null) + return false; + if(ai.empire.SubjugatedBy !is null) + return false; + if(!contacted) + return false; + if(ai.empire.ForcedPeaceMask & empire.mask != 0) + return false; + return true; + } +}; + +class Relations : AIComponent { + Intelligence@ intelligence; + Systems@ systems; + Fleets@ fleets; + Planets@ planets; + + array relations; + + bool expansionLocked = false; + double treatyRespond = 0; + double treatyConsider = 0; + + double warPoints = 0.0; + + void create() { + @intelligence = cast(ai.intelligence); + @fleets = cast(ai.fleets); + @systems = cast(ai.systems); + @planets = cast(ai.planets); + } + + void start() { + for(uint i = 0, cnt = getEmpireCount(); i < cnt; ++i) { + Empire@ emp = getEmpire(i); + if(emp is ai.empire) + continue; + if(!emp.major) + continue; + + Relation r; + @r.empire = emp; + + if(relations.length <= uint(emp.index)) + relations.length = uint(emp.index)+1; + @relations[emp.index] = r; + } + } + + void save(SaveFile& file) { + uint cnt = relations.length; + file << cnt; + for(uint i = 0; i < cnt; ++i) { + if(relations[i] is null) { + file.write0(); + continue; + } + + file.write1(); + relations[i].save(this, file); + } + + file << expansionLocked; + file << treatyRespond; + file << treatyConsider; + } + + void load(SaveFile& file) { + uint cnt = 0; + file >> cnt; + relations.length = cnt; + + for(uint i = 0; i < cnt; ++i) { + if(!file.readBit()) + continue; + + @relations[i] = Relation(); + @relations[i].empire = getEmpire(i); + relations[i].load(this, file); + } + + file >> expansionLocked; + file >> treatyRespond; + file >> treatyConsider; + } + + double getPointValue(Object@ obj) { + if(obj is null) + return 0.0; + if(obj.isShip) { + auto@ dsg = cast(obj).blueprint.design; + if(dsg !is null) + return dsg.size; + } + else if(obj.isPlanet) { + return 10.0 * pow(3.0, double(obj.level)); + } + return 0.0; + } + + void recordTakenFrom(Empire& emp, double amount) { + if(!emp.valid) + return; + if(log) + ai.print("Taken value "+amount+" from "+emp.name); + auto@ rel = get(emp); + if(rel !is null) + rel.warTaken += amount; + } + + void recordLostTo(Empire& emp, double amount) { + if(!emp.valid) + return; + if(log) + ai.print("Lost value "+amount+" to "+emp.name); + auto@ rel = get(emp); + if(rel !is null) + rel.warLost += amount; + } + + void recordLostTo(Empire& emp, Object@ obj) { + recordLostTo(emp, getPointValue(obj)); + } + + void recordTakenFrom(Empire& emp, Object@ obj) { + recordTakenFrom(emp, getPointValue(obj)); + } + + Relation@ get(Empire@ emp) { + if(emp is null) + return null; + if(!emp.major) + return null; + if(uint(emp.index) >= relations.length) + return null; + return relations[emp.index]; + } + + bool isFightingWar(bool aggressive = false) { + for(uint i = 0, cnt = relations.length; i < cnt; ++i) { + if(relations[i] is null) + continue; + if(relations[i].atWar) { + if(!aggressive || relations[i].aggressive) + return true; + } + } + return false; + } + + uint warCount() { + uint count = 0; + for(uint i = 0, cnt = relations.length; i < cnt; ++i) { + if(relations[i] is null) + continue; + if(relations[i].atWar) + count += 1; + } + return count; + } + + void declareWar(Empire@ onEmpire, bool aggressive = true) { + //Break all treaties + leaveTreatiesWith(ai.empire, onEmpire.mask); + + //Declare actual war + auto@ rel = get(onEmpire); + rel.aggressive = aggressive; + ::declareWar(ai.empire, onEmpire); + } + + uint sysIdx = 0; + uint relIdx = 0; + void tick(double time) override { + //Find new ways to hate other empires + if(systems.all.length != 0) { + sysIdx = (sysIdx+1) % systems.all.length; + auto@ sys = systems.all[sysIdx]; + if(sys.owned && sys.seenPresent & ~ai.mask != 0) { + for(uint i = 0, cnt = relations.length; i < cnt; ++i) { + auto@ rel = relations[i]; + if(rel is null) + continue; + if(sys.seenPresent & rel.empire.mask != 0) + rel.trackSystem(ai, this, sys); + } + } + } + + if(relations.length != 0) { + relIdx = (relIdx+1) % relations.length; + auto@ rel = relations[relIdx]; + auto@ itl = intelligence.intel[relIdx]; + if(rel !is null && itl !is null) { + for(uint i = 0, cnt = itl.fleets.length; i < cnt; ++i) { + if(!itl.fleets[i].visible) + continue; + + auto@ inSys = systems.getAI(itl.fleets[i].obj.region); + if(inSys !is null && inSys.owned) + rel.trackFleet(ai, this, itl.fleets[i], inSys); + } + } + } + } + + uint relInd = 0; + void focusTick(double time) override { + //Update our current relations + if(relations.length != 0) { + relInd = (relInd+1) % relations.length; + if(relations[relInd] !is null) + relations[relInd].tick(ai, this, time); + } + + //Compute how many points we have in total that can be taken + warPoints = 0.0; + for(uint i = 0, cnt = fleets.fleets.length; i < cnt; ++i) { + Ship@ ship = cast(fleets.fleets[i].obj); + if(ship !is null && ship.valid && ship.owner is ai.empire) + warPoints += getPointValue(ship); + } + for(uint i = 0, cnt = planets.planets.length; i < cnt; ++i) { + Planet@ pl = cast(planets.planets[i].obj); + if(pl !is null && pl.valid && pl.owner is ai.empire) + warPoints += getPointValue(pl); + } + + //Become aggressive if we cannot expand anywhere anymore + expansionLocked = true; + for(uint i = 0, cnt = systems.outsideBorder.length; i < cnt; ++i) { + auto@ sys = systems.outsideBorder[i]; + if(sys.seenPresent == 0) { + bool havePlanets = false; + for(uint n = 0, ncnt = sys.planets.length; n < ncnt; ++n) { + if(sys.planets[n].quarantined) + continue; + havePlanets = true; + break; + } + if(havePlanets) { + expansionLocked = false; + break; + } + } + } + + if(ai.behavior.forbidDiplomacy) return; + + //Deal with our AI's aggressive behavior + if(ai.behavior.aggressive || (expansionLocked && ai.behavior.aggressiveWhenBoxedIn && !ai.behavior.passive)) { + //Try to make sure we're always fighting at least one aggressive war + bool atWar = false, aggro = false; + for(uint i = 0, cnt = relations.length; i < cnt; ++i) { + if(relations[i] is null) + continue; + if(relations[i].atWar) { + atWar = true; + if(relations[i].aggressive) + aggro = true; + } + } + + if(!atWar) { + if(fleets.haveCombatReadyFleets()) { + //Declare war on people who share our border and are defeatable + Empire@ best; + double bestWeight = 0; + + for(uint i = 0, cnt = relations.length; i < cnt; ++i) { + auto@ rel = relations[i]; + if(rel is null) + continue; + + auto@ intel = intelligence.get(rel.empire); + if(intel.shared.length == 0 && intel.theirBorder.length == 0) + continue; + if(!rel.canDeclareWar(ai)) + continue; + if(!ai.behavior.biased || rel.empire.isAI) { + if(rel.defeatable < ai.behavior.aggressiveWarOverkill) + continue; + } + + double w = rel.defeatable * rel.hate; + if(rel.isAllied(ai)) + w *= 0.01; + if(w > bestWeight) { + bestWeight = w; + @best = rel.empire; + } + } + + if(best !is null) { + if(log) + ai.print("Declare aggressive war against "+best.name); + declareWar(best, aggressive=true); + } + } + } + else if(!aggro) { + //Start going aggressive on someone defeatable we are already at war with + Empire@ best; + double bestWeight = 0; + + for(uint i = 0, cnt = relations.length; i < cnt; ++i) { + auto@ rel = relations[i]; + if(rel is null) + continue; + if(!rel.atWar) + continue; + if(rel.defeatable < ai.behavior.aggressiveWarOverkill) + continue; + + double w = rel.defeatable * rel.hate; + if(w > bestWeight) { + bestWeight = w; + @best = rel.empire; + } + } + + if(best !is null) { + //Go aggressive then! + if(log) + ai.print("Become aggressive against "+best.name); + get(best).aggressive = true; + } + } + } + + //Respond to treaties + if(gameTime > treatyRespond) { + treatyRespond = gameTime + randomd(8.0, 20.0); + + Treaty@ respondTreaty; + + { + Lock lck(influenceLock); + for(uint i = 0, cnt = activeTreaties.length; i < cnt; ++i) { + auto@ trty = activeTreaties[i]; + if(trty.inviteMask & ai.mask != 0 && trty.presentMask & ai.mask == 0) { + Message msg; + trty.write(msg); + + @respondTreaty = Treaty(); + respondTreaty.read(msg); + break; + } + } + } + + if(respondTreaty !is null) { + bool accept = false; + Empire@ invitedBy = respondTreaty.leader; + if(invitedBy is null) + @invitedBy = respondTreaty.joinedEmpires[0]; + Relation@ other = get(invitedBy); + + if(respondTreaty.hasClause("SubjugateClause")) { + //This is a surrender offer or demand + if(respondTreaty.leader is null) { + //An offer + accept = true; + } + else if(respondTreaty.joinedEmpires.length != 0) { + //A demand + auto@ other = get(respondTreaty.joinedEmpires[0]); + if(other.defeatable < ai.behavior.surrenderMinStrength) { + if(warPoints / (other.warLost + warPoints) < ai.behavior.acceptSurrenderRatio) { + accept = true; + } + } + } + } + else if(respondTreaty.hasClause("MutualDefenseClause") + || respondTreaty.hasClause("AllianceClause")) { + //This is an alliance treaty + if(other.atWar) { + //Need to be at peace first + accept = false; + } + else { + //See if this empire can help us defeat someone + if(other.allyValue >= 3.0 && other.relStrength >= 0.5) + accept = true; + } + } + else if(respondTreaty.hasClause("PeaceClause")) { + //This is a peace offering + accept = shouldPeace(other); + } + else if(respondTreaty.hasClause("VisionClause")) { + //This is a vision sharing treaty + if(other !is null) + accept = !other.isThreat && !other.atWar && other.hate <= 50.0; + } + else if(respondTreaty.hasClause("TradeClause")) { + //This is a trade sharing treaty + if(other !is null) + accept = !other.isThreat && !other.atWar && other.hate <= 10.0; + } + + if(accept) { + if(log) + ai.print("Accept treaty: "+respondTreaty.name, emp=invitedBy); + joinTreaty(ai.empire, respondTreaty.id); + } + else { + if(log) + ai.print("Reject treaty: "+respondTreaty.name, emp=invitedBy); + declineTreaty(ai.empire, respondTreaty.id); + } + } + } + + //See if we should send a treaty over to someone + if(gameTime > treatyConsider) { + treatyConsider = gameTime + randomd(100.0, 300.0); + + uint offset = randomi(0, relations.length-1); + for(uint i = 0, cnt = relations.length; i < cnt; ++i) { + auto@ other = relations[(i+offset) % cnt]; + if(other is null) + continue; + + //Check if we should make peace with them + if(other.atWar) { + if(other.lastPeaceTry < gameTime - 600.0 && shouldPeace(other, isOffer=true)) { + if(other.aggressive) + other.aggressive = false; + if(log) + ai.print("Send peace offer.", emp=other.empire); + other.lastPeaceTry = gameTime; + sendPeaceOffer(ai.empire, other.empire); + break; + } + } + + if(other.atWar) { + //Check if we should try to surrender to them + if(other.defeatable < ai.behavior.surrenderMinStrength) { + if(warPoints / (other.warLost + warPoints) < ai.behavior.offerSurrenderRatio) { + if(log) + ai.print("Send surrender offer.", emp=other.empire); + offerSurrender(ai.empire, other.empire); + break; + } + } + + //Check if we should try to demand their surrender + if(other.defeatable >= 1.0 / ai.behavior.surrenderMinStrength && other.warTaken >= warPoints * 0.1) { + if(log) + ai.print("Demand surrender.", emp=other.empire); + demandSurrender(ai.empire, other.empire); + break; + } + } + + //Check if we should try to ally with them + if(!other.atWar && !other.isThreat && other.allyValue >= 3.0) { + Treaty treaty; + treaty.addClause(getInfluenceClauseType("AllianceClause")); + treaty.addClause(getInfluenceClauseType("VisionClause")); + treaty.addClause(getInfluenceClauseType("MutualDefenseClause")); + + if(treaty.canInvite(ai.empire, other.empire)) { + treaty.inviteMask = other.empire.mask; + + //Generate treaty name + string genName; + uint genCount = 0; + for(uint i = 0, cnt = systemCount; i < cnt; ++i) { + auto@ reg = getSystem(i).object; + if(reg.TradeMask & (ai.mask | other.empire.mask) != 0) { + genCount += 1; + if(randomd() < 1.0 / double(genCount)) + genName = reg.name; + } + } + treaty.name = format(locale::TREATY_NAME_GEN, genName); + + if(log) + ai.print("Send alliance offer.", emp=other.empire); + createTreaty(ai.empire, treaty); + } + } + } + } + } + + bool shouldPeace(Relation@ other, bool isOffer = false) { + bool accept = false; + if(other.aggressive) { + //We're trying to conquer these people, don't accept peace unless + //we're fighting someone scarier or we're losing + double otherWar = 0.0; + uint otherInd = uint(-1); + for(uint i = 0, cnt = relations.length; i < cnt; ++i) { + auto@ rel = relations[i]; + if(rel is null || rel is other) + continue; + if(rel.empire.mask & other.alliedTo != 0) + continue; + if(!rel.atWar) + continue; + otherWar = max(otherWar, rel.defeatable); + otherInd = i; + } + + if(otherInd != uint(-1) && otherWar < other.defeatable) { + accept = true; + if(!relations[otherInd].aggressive) + relations[otherInd].aggressive = otherWar >= ai.behavior.aggressiveWarOverkill; + } + else if(other.defeatable < 0.25) { + accept = true; + } + } + else { + //We don't have any ~particular qualms with these people, peace should be good + if(!isOffer) { + if(other.defeatable < 0.5 || other.hate < 50.0) + accept = true; + } + } + return accept; + } + + void turn() override { + if(log) { + ai.print("Relations Report on Empires:"); + ai.print(" war points: "+warPoints); + for(uint i = 0, cnt = relations.length; i < cnt; ++i) { + auto@ rel = relations[i]; + if(rel is null) + continue; + ai.print(" "+ai.pad(rel.empire.name, 15) + +" war: "+ai.pad(rel.atWar+" / "+rel.aggressive, 15) + +" threat: "+ai.pad(""+rel.isThreat, 8) + +" defeatable: "+ai.pad(toString(rel.defeatable,2), 8) + +" hate: "+ai.pad(toString(rel.hate,0), 8) + +" ally value: "+ai.pad(toString(rel.allyValue,1), 8) + +" taken: "+ai.pad(toString(rel.warTaken,1), 8) + +" lost: "+ai.pad(toString(rel.warLost,1), 8) + ); + } + } + } +}; + +AIComponent@ createRelations() { + return Relations(); +} + +void relationRecordLost(AI& ai, Empire& emp, Object@ obj) { + cast(ai.relations).recordLostTo(emp, obj); +} ADDED scripts/server/empire_ai/weasel/Research.as Index: scripts/server/empire_ai/weasel/Research.as ================================================================== --- scripts/server/empire_ai/weasel/Research.as +++ scripts/server/empire_ai/weasel/Research.as @@ -0,0 +1,215 @@ +// Research +// -------- +// Spends research points to unlock and improve things in the research grid. +// + +import empire_ai.weasel.WeaselAI; + +import empire_ai.weasel.Development; + +import research; + +const double baseAimResearchRate = 2.0; + +class Research : AIComponent { + Development@ development; + + TechnologyGrid grid; + array immediateQueue; + + void create() { + @development = cast(ai.development); + } + + void save(SaveFile& file) { + uint cnt = immediateQueue.length; + file << cnt; + for(uint i = 0; i < cnt; ++i) + file << immediateQueue[i].id; + } + + void load(SaveFile& file) { + updateGrid(); + + uint cnt = 0; + file >> cnt; + for(uint i = 0; i < cnt; ++i) { + int id = 0; + file >> id; + + for(uint i = 0, cnt = grid.nodes.length; i < cnt; ++i) { + if(grid.nodes[i].id == id) { + immediateQueue.insertLast(grid.nodes[i]); + break; + } + } + } + + } + + void updateGrid() { + //Receive the full grid from the empire to path on + grid.nodes.length = 0; + + DataList@ recvData = ai.empire.getTechnologyNodes(); + TechnologyNode@ node = TechnologyNode(); + while(receive(recvData, node)) { + grid.nodes.insertLast(node); + @node = TechnologyNode(); + } + + grid.regenBounds(); + } + + double getEndPointWeight(const TechnologyType& tech) { + //TODO: Might want to make this configurable by data file + return 1.0; + } + + bool isEndPoint(const TechnologyType& tech) { + return tech.cls >= Tech_BigUpgrade; + } + + double findResearch(int atIndex, array& path, array& visited, bool initial = false) { + if(visited[atIndex]) + return 0.0; + visited[atIndex] = true; + + auto@ node = grid.nodes[atIndex]; + if(!initial) { + if(node.bought) + return 0.0; + if(!node.hasRequirements(ai.empire)) + return 0.0; + + path.insertLast(node); + + if(isEndPoint(node.type)) + return getEndPointWeight(node.type); + } + + vec2i startPos = node.position; + double totalWeight = 0.0; + + array tmp; + array chosen; + tmp.reserve(20); + chosen.reserve(20); + + for(uint d = 0; d < 6; ++d) { + vec2i otherPos = startPos; + if(grid.doAdvance(otherPos, HexGridAdjacency(d))) { + int otherIndex = grid.getIndex(otherPos); + if(otherIndex != -1) { + tmp.length = 0; + double w = findResearch(otherIndex, tmp, visited); + if(w != 0.0) { + totalWeight += w; + if(randomd() < w / totalWeight) { + chosen = tmp; + } + } + } + } + } + + for(uint i = 0, cnt = chosen.length; i < cnt; ++i) + path.insertLast(chosen[i]); + return max(totalWeight, 0.01); + } + + void queueNewResearch() { + if(log) + ai.print("Attempted to find new research to queue"); + + //Update our grid representation + updateGrid(); + + //Find a good path to do + array visited(grid.nodes.length, false); + + double totalWeight = 0.0; + + auto@ path = array(); + auto@ tmp = array(); + path.reserve(20); + tmp.reserve(20); + + for(int i = 0, cnt = grid.nodes.length; i < cnt; ++i) { + if(grid.nodes[i].bought) { + tmp.length = 0; + double weight = findResearch(i, tmp, visited, initial=true); + if(weight != 0.0) { + totalWeight += weight; + if(randomd() < weight / totalWeight) { + auto@ swp = path; + @path = tmp; + @tmp = swp; + } + } + } + } + + if(path.length != 0) { + for(uint i = 0, cnt = path.length; i < cnt; ++i) { + if(log) + ai.print("Queue research: "+path[i].type.name+" at "+path[i].position); + immediateQueue.insertLast(path[i]); + } + } + } + + double immTimer = randomd(10.0, 60.0); + void focusTick(double time) override { + if (ai.behavior.forbidResearch) return; + + //Queue some new research if we have to + if(immediateQueue.length == 0) { + immTimer -= time; + if(immTimer <= 0.0) { + immTimer = 60.0; + queueNewResearch(); + } + } + else { + immTimer = 0.0; + } + + //Deal with current queued research + if(immediateQueue.length != 0) { + auto@ node = immediateQueue[0]; + if(!receive(ai.empire.getTechnologyNode(node.id), node)) { + immediateQueue.removeAt(0); + } + else if(!node.available || node.bought) { + immediateQueue.removeAt(0); + } + else { + double cost = node.getPointCost(ai.empire); + if(cost == 0) { + //Try it once and then give up + ai.empire.research(node.id, secondary=true); + immediateQueue.removeAt(0); + + if(log) + ai.print("Attempt secondary research: "+node.type.name+" at "+node.position); + } + else if(cost <= ai.empire.ResearchPoints) { + //If we have enough to buy it, buy it + ai.empire.research(node.id); + immediateQueue.removeAt(0); + + if(log) + ai.print("Purchase research: "+node.type.name+" at "+node.position); + } + } + } + + //Update research generation rate goal + development.aimResearchRate = clamp(gameTime / (20.0 * 60.0) - 0.5, 0.0, baseAimResearchRate); + } +}; + +AIComponent@ createResearch() { + return Research(); +} ADDED scripts/server/empire_ai/weasel/Resources.as Index: scripts/server/empire_ai/weasel/Resources.as ================================================================== --- scripts/server/empire_ai/weasel/Resources.as +++ scripts/server/empire_ai/weasel/Resources.as @@ -0,0 +1,713 @@ +import empire_ai.weasel.WeaselAI; + +import empire_ai.weasel.Events; + +import empire_ai.weasel.ImportData; + +import ai.events; + +import resources; +import planet_levels; +import system_pathing; +import systems; + +from orbitals import OrbitalModule; + +interface RaceResources { + void levelRequirements(Object& obj, int targetLevel, array& specs); +}; + +final class Resources : AIComponent { + Events@ events; + + RaceResources@ race; + + array requested; + array active; + int nextImportId = 0; + + array available; + array used; + int nextExportId = 0; + + void create() { + @events = cast(ai.events); + @race = cast(ai.race); + } + + void save(SaveFile& file) { + file << nextImportId; + file << nextExportId; + + uint cnt = 0; + + cnt = requested.length; + file << cnt; + for(uint i = 0; i < cnt; ++i) { + saveImport(file, requested[i]); + file << requested[i]; + } + + cnt = active.length; + file << cnt; + for(uint i = 0; i < cnt; ++i) { + saveImport(file, active[i]); + file << active[i]; + } + + cnt = available.length; + file << cnt; + for(uint i = 0; i < cnt; ++i) { + saveExport(file, available[i]); + file << available[i]; + saveImport(file, available[i].request); + } + + cnt = used.length; + file << cnt; + for(uint i = 0; i < cnt; ++i) { + saveExport(file, used[i]); + file << used[i]; + saveImport(file, used[i].request); + } + } + + void load(SaveFile& file) { + file >> nextImportId; + file >> nextExportId; + + uint cnt = 0; + + file >> cnt; + for(uint i = 0; i < cnt; ++i) { + auto@ data = loadImport(file); + file >> data; + requested.insertLast(data); + } + + file >> cnt; + for(uint i = 0; i < cnt; ++i) { + auto@ data = loadImport(file); + file >> data; + active.insertLast(data); + } + + file >> cnt; + for(uint i = 0; i < cnt; ++i) { + auto@ data = loadExport(file); + file >> data; + @data.request = loadImport(file); + available.insertLast(data); + } + + file >> cnt; + for(uint i = 0; i < cnt; ++i) { + auto@ data = loadExport(file); + file >> data; + @data.request = loadImport(file); + used.insertLast(data); + } + } + + array importIds; + ImportData@ loadImport(int id) { + if(id == -1) + return null; + for(uint i = 0, cnt = importIds.length; i < cnt; ++i) { + if(importIds[i].id == id) + return importIds[i]; + } + ImportData data; + data.id = id; + importIds.insertLast(data); + return data; + } + ImportData@ loadImport(SaveFile& file) { + int id = -1; + file >> id; + if(id == -1) + return null; + else + return loadImport(id); + } + void saveImport(SaveFile& file, ImportData@ data) { + int id = -1; + if(data !is null) + id = data.id; + file << id; + } + array exportIds; + ExportData@ loadExport(int id) { + if(id == -1) + return null; + for(uint i = 0, cnt = exportIds.length; i < cnt; ++i) { + if(exportIds[i].id == id) + return exportIds[i]; + } + ExportData data; + data.id = id; + exportIds.insertLast(data); + return data; + } + ExportData@ loadExport(SaveFile& file) { + int id = -1; + file >> id; + if(id == -1) + return null; + else + return loadExport(id); + } + void saveExport(SaveFile& file, ExportData@ data) { + int id = -1; + if(data !is null) + id = data.id; + file << id; + } + void postLoad(AI& ai) { + importIds.length = 0; + exportIds.length = 0; + } + + void start() { + focusTick(0); + } + + void tick(double time) { + } + + uint checkIdx = 0; + void focusTick(double time) { + //Do a check to make sure our resource export setup is still correct + if(used.length != 0) { + checkIdx = (checkIdx+1) % used.length; + ExportData@ res = used[checkIdx]; + if(res.request !is null && res.request.obj !is null && !res.isExportedTo(res.request.obj)) { + if(log) + ai.print("Break export to "+res.request.obj.name+": link changed underfoot", res.obj); + breakImport(res); + } + else { + bool valid = true; + if(res.obj is null || res.obj.owner !is ai.empire || !res.obj.valid) + valid = false; + //Don't break these imports, we want to wait for the decay to happen + else if((res.request is null || !res.request.obj.hasSurfaceComponent || res.request.obj.decayTime <= 0) && !res.obj.isAsteroid && !res.usable) { + valid = false; + } + else if(res.request !is null) { + if(res.request.obj.owner !is ai.empire || !res.request.obj.valid) + valid = false; + } + if(!valid) + breakImport(res); + } + + } + + //TODO: Make sure universal unique only applies once per planet + + //Match requested with available + for(uint i = 0, cnt = requested.length; i < cnt; ++i) { + auto@ req = requested[i]; + req.cycled = true; + + if(req.obj is null) + continue; + if(req.beingMet) { + ai.print("Error: Requested is being met", req.obj); + continue; + } + + ExportData@ source; + double sourceWeight = 0.0; + + for(uint j = 0, jcnt = available.length; j < jcnt; ++j) { + auto@ av = available[j]; + if(av.request !is null) { + ai.print("Error: Available is being used", av.obj); + continue; + } + + if(!req.spec.meets(av.resource, av.obj, req.obj)) + continue; + if(!av.usable || av.obj is null || !av.obj.valid || av.obj.owner !is ai.empire) + continue; + //Check if a trade route exists between the two locations + if(!canTradeBetween(av.obj, req.obj) && av.obj.region !is null && req.obj.region !is null) { + auto@ territoryA = av.obj.region.getTerritory(ai.empire); + auto@ territoryB = req.obj.region.getTerritory(ai.empire); + if (territoryA !is territoryB) { + if (log) + ai.print("trade route requested between " + addrstr(territoryA) + " and " + addrstr(territoryB)); + events.notifyTradeRouteNeeded(this, TradeRouteNeededEventArgs(territoryA, territoryB)); + } + continue; + } + if(av.localOnly && av.obj !is req.obj) + continue; + + double weight = 1.0; + if(req.obj is av.obj) + weight = INFINITY; + + if(weight > sourceWeight) { + sourceWeight = weight; + @source = av; + } + } + + if(source !is null) { + link(req, source); + --i; --cnt; + } + } + } + + void turn() { + } + + bool get_hasOpenRequests() { + for(uint i = 0, cnt = requested.length; i < cnt; ++i) { + auto@ req = requested[i]; + if(req.isOpen) + return true; + } + return false; + } + + TradePath tradePather; + int tradeDistance(Region& fromRegion, Region& toRegion) { + @tradePather.forEmpire = ai.empire; + tradePather.generate(getSystem(fromRegion), getSystem(toRegion), keepCache=true); + if(!tradePather.valid) + return -1; + return tradePather.pathSize - 1; + } + + bool canTradeBetween(Object& fromObj, Object& toObj) { + Region@ fromRegion = fromObj.region; + if(fromRegion is null) + return false; + Region@ toRegion = toObj.region; + if(toRegion is null) + return false; + return canTradeBetween(fromRegion, toRegion); + } + + bool canTradeBetween(Region& fromRegion, Region& toRegion) { + if(fromRegion.sharesTerritory(ai.empire, toRegion)) + return true; + int dist = tradeDistance(fromRegion, toRegion); + if(dist < 0) + return false; + return true; + } + + void link(ImportData@ req, ExportData@ source) { + //Manage the data + @source.request = req; + @source.developUse = null; + req.set(source); + + requested.remove(req); + active.insertLast(req); + + req.beingMet = true; + + available.remove(source); + used.insertLast(source); + + if(log) + ai.print("link "+source.resource.name+" from "+source.obj.name+" to "+req.obj.name); + + //Perform the actual export + if(source.obj !is req.obj) + source.obj.exportResourceByID(source.resourceId, req.obj); + else + source.obj.exportResourceByID(source.resourceId, null); + } + + ImportData@ requestResource(Object& toObject, ResourceSpec& spec, bool forLevel = false, bool activate = true, bool prioritize = false) { + ImportData data; + data.idleSince = gameTime; + data.id = nextImportId++; + @data.obj = toObject; + @data.spec = spec; + data.forLevel = forLevel; + + if(log) + ai.print("requested resource: "+spec.dump(), toObject); + + if(activate) { + if(prioritize) + requested.insertAt(0, data); + else + requested.insertLast(data); + } + return data; + } + + ExportData@ availableResource(Object& fromObject, const ResourceType& resource, int id) { + ExportData data; + data.id = nextExportId++; + @data.obj = fromObject; + @data.resource = resource; + data.resourceId = id; + + if(log) + ai.print("available resource: "+resource.name, fromObject); + + available.insertLast(data); + return data; + } + + void checkReplaceCurrent(ExportData@ res) { + //If the planet that this resource is on is currently importing this same resource, switch it around + if(res.request !is null) + return; + + for(uint i = 0, cnt = used.length; i < cnt; ++i) { + auto@ other = used[i]; + auto@ request = other.request; + if(request is null) + continue; + if(request.obj !is res.obj) + continue; + + if(request.spec.meets(res.resource, res.obj, res.obj)) { + //Swap the import with using the local resource + if(other.resource.exportable) { + breakImport(other); + link(request, res); + return; + } + } + } + } + + array checkResources; + array@ availableResource(Object& fromObject) { + array list; + + checkResources.syncFrom(fromObject.getNativeResources()); + + uint nativeCount = checkResources.length; + for(uint i = 0; i < nativeCount; ++i) { + auto@ r = checkResources[i].type; + if(r !is null) + list.insertLast(availableResource(fromObject, r, checkResources[i].id)); + } + + return list; + } + + ExportData@ findResource(Object@ obj, int resourceId) { + for(uint i = 0, cnt = available.length; i < cnt; ++i) { + if(available[i].obj is obj && available[i].resourceId == resourceId) + return available[i]; + } + for(uint i = 0, cnt = used.length; i < cnt; ++i) { + if(used[i].obj is obj && used[i].resourceId == resourceId) + return used[i]; + } + return null; + } + + ImportData@ findUnclaimed(ExportData@ forResource) { + for(uint i = 0, cnt = requested.length; i < cnt; ++i) { + auto@ req = requested[i]; + if(req.claimedFor) + continue; + if(req.beingMet) + continue; + + if(!req.spec.meets(forResource.resource, forResource.obj, req.obj)) + continue; + if(!canTradeBetween(req.obj, forResource.obj)) + continue; + + return req; + } + return null; + } + + void breakImport(ImportData@ data) { + if(data.fromObject !is null) { + auto@ source = findResource(data.fromObject, data.resourceId); + if(source !is null) { + breakImport(source); + return; + } + } + + @data.fromObject = null; + data.resourceId = -1; + data.beingMet = false; + data.idleSince = gameTime; + + active.remove(data); + requested.insertAt(0, data); + } + + void breakImport(ExportData@ data) { + if(data.request !is null) { + if(data.request.obj !is data.obj) + data.obj.exportResource(data.resourceId, null); + + data.request.beingMet = false; + @data.request.fromObject = null; + data.request.resourceId = -1; + data.request.idleSince = gameTime; + + active.remove(data.request); + requested.insertAt(0, data.request); + + @data.request = null; + } + + used.remove(data); + available.insertLast(data); + } + + void cancelRequest(ImportData@ data) { + if(data.beingMet) { + breakImport(data); + active.remove(data); + } + else { + requested.remove(data); + } + } + + void removeResource(ExportData@ data) { + if(data.request !is null) { + breakImport(data); + used.remove(data); + @data.obj = null; + } + else { + available.remove(data); + @data.obj = null; + } + } + + ImportData@ claimImport(ImportData@ data) { + data.beingMet = true; + requested.remove(data); + active.insertLast(data); + return data; + } + + void relinquishImport(ImportData@ data) { + data.beingMet = false; + active.remove(data); + requested.insertLast(data); + } + + void organizeImports(Object& obj, int targetLevel, ImportData@ before = null) { + //Organize any imports for this object so it tries to get to a particular target level + if(log) + ai.print("Organizing imports for level", obj, targetLevel); + + //Get the requirement list + const PlanetLevel@ lvl = getPlanetLevel(obj, targetLevel); + if(lvl is null) { + ai.print("Error: could not find planet level", obj, targetLevel); + return; //Welp, can't do nothing here + } + + //Collect all the requests this planet currently has outstanding + array activeRequests; + for(uint i = 0, cnt = requested.length; i < cnt; ++i) { + auto@ req = requested[i]; + if(req.obj !is obj) + continue; + if(!req.forLevel) + continue; + + activeRequests.insertLast(req); + } + for(uint i = 0, cnt = active.length; i < cnt; ++i) { + auto@ req = active[i]; + if(req.obj !is obj) + continue; + if(!req.forLevel) + continue; + + activeRequests.insertLast(req); + } + + //TODO: This needs to be able to deal with dummy resources + + //Match import requests with level requirements + array addSpecs; + const ResourceRequirements@ reqs = lvl.reqs; + + for(uint i = 0, cnt = reqs.reqs.length; i < cnt; ++i) { + auto@ need = reqs.reqs[i]; + for(uint n = 0; n < need.amount; ++n) + addSpecs.insertLast(implementSpec(need)); + } + + if(race !is null) + race.levelRequirements(obj, targetLevel, addSpecs); + + for(uint i = 0, cnt = addSpecs.length; i < cnt; ++i) { + auto@ spec = addSpecs[i]; + + bool foundMatch = false; + for(uint j = 0, jcnt = activeRequests.length; j < jcnt; ++j) { + if(activeRequests[j].spec == spec) { + foundMatch = true; + activeRequests.removeAt(j); + break; + } + } + + if(foundMatch) { + addSpecs.removeAt(i); + --i; --cnt; + } + } + + //Cancel any import requests that we don't need anymore + for(uint i = 0, cnt = activeRequests.length; i < cnt; ++i) + cancelRequest(activeRequests[i]); + + //Insert any imports above any imports of the planet we're exporting to + int place = -1; + if(before !is null) { + for(uint i = 0, cnt = requested.length; i < cnt; ++i) { + if(requested[i] is before) { + place = int(i); + break; + } + } + } + + //Insert everything we need to add + addSpecs.sortDesc(); + for(uint i = 0, cnt = addSpecs.length; i < cnt; ++i) { + ImportData@ req = requestResource(obj, addSpecs[i], forLevel=true, activate=false); + if(place == -1) { + requested.insertLast(req); + } + else { + requested.insertAt(place, req); + place += 1; + } + } + } + + void killImportsTo(Object& obj) { + for(uint i = 0, cnt = requested.length; i < cnt; ++i) { + if(requested[i].obj is obj) { + cancelRequest(requested[i]); + --i; --cnt; + } + } + for(uint i = 0, cnt = active.length; i < cnt; ++i) { + if(active[i].obj is obj) { + cancelRequest(active[i]); + --i; --cnt; + } + } + } + + void killResourcesFrom(Object& obj) { + for(uint i = 0, cnt = available.length; i < cnt; ++i) { + if(available[i].obj is obj) { + removeResource(available[i]); + --i; --cnt; + } + } + for(uint i = 0, cnt = used.length; i < cnt; ++i) { + if(used[i].obj is obj) { + removeResource(used[i]); + --i; --cnt; + } + } + } + + ImportData@ getImport(const string& fromName, uint index = 0) { + for(uint i = 0, cnt = requested.length; i < cnt; ++i) { + if(requested[i].obj.name.equals_nocase(fromName)) { + if(index == 0) + return requested[i]; + else + index -= 1; + } + } + for(uint i = 0, cnt = active.length; i < cnt; ++i) { + if(active[i].obj.name.equals_nocase(fromName)) { + if(index == 0) + return active[i]; + else + index -= 1; + } + } + return null; + } + + ExportData@ getExport(const string& fromName, uint index = 0) { + for(uint i = 0, cnt = available.length; i < cnt; ++i) { + if(available[i].obj.name.equals_nocase(fromName)) { + if(index == 0) + return available[i]; + else + index -= 1; + } + } + for(uint i = 0, cnt = used.length; i < cnt; ++i) { + if(used[i].obj.name.equals_nocase(fromName)) { + if(index == 0) + return used[i]; + else + index -= 1; + } + } + return null; + } + + void getImportsOf(array& output, uint resType, Planet@ toPlanet = null) { + for(uint i = 0, cnt = active.length; i < cnt; ++i) { + auto@ req = active[i]; + if(req.spec.type != RST_Specific) + continue; + if(req.spec.resource.id != resType) + continue; + if(toPlanet !is null && req.obj !is toPlanet) + continue; + output.insertLast(req); + } + for(uint i = 0, cnt = requested.length; i < cnt; ++i) { + auto@ req = requested[i]; + if(req.spec.type != RST_Specific) + continue; + if(req.spec.resource.id != resType) + continue; + if(toPlanet !is null && req.obj !is toPlanet) + continue; + output.insertLast(req); + } + } + + void dumpRequests(Object@ forObject = null) { + for(uint i = 0, cnt = requested.length; i < cnt; ++i) { + if(forObject !is null && requested[i].obj !is forObject) + continue; + print(requested[i].obj.name+" requests "+requested[i].spec.dump()); + } + if(forObject !is null) { + for(uint i = 0, cnt = used.length; i < cnt; ++i) { + if(used[i].request is null || used[i].request.obj !is forObject) + continue; + print(used[i].request.obj.name+" is getting "+used[i].request.spec.dump()+" from "+used[i].obj.name); + } + } + } +}; + +AIComponent@ createResources() { + return Resources(); +} ADDED scripts/server/empire_ai/weasel/Scouting.as Index: scripts/server/empire_ai/weasel/Scouting.as ================================================================== --- scripts/server/empire_ai/weasel/Scouting.as +++ scripts/server/empire_ai/weasel/Scouting.as @@ -0,0 +1,507 @@ +// Scouting +// -------- +// Orders the construction of scouts, explores the galaxy with them and makes +// sure we have vision where we need vision, as well as scanning anomalies. +// + +import empire_ai.weasel.WeaselAI; + +import empire_ai.weasel.Fleets; +import empire_ai.weasel.Systems; +import empire_ai.weasel.Designs; +import empire_ai.weasel.Construction; +import empire_ai.weasel.Movement; +import empire_ai.weasel.Creeping; + +final class ScoutingMission : Mission { + Region@ region; + MoveOrder@ move; + + void save(Fleets& fleets, SaveFile& file) override { + file << region; + fleets.movement.saveMoveOrder(file, move); + } + + void load(Fleets& fleets, SaveFile& file) override { + file >> region; + @move = fleets.movement.loadMoveOrder(file); + } + + double getPerformWeight(AI& ai, FleetAI& fleet) { + if(fleet.fleetClass != FC_Scout) { + if(fleet.fleetClass == FC_Mothership) + return 0.0; + if(gameTime > ai.behavior.scoutAllTimer) + return 0.0; + } + return 1.0 / region.position.distanceTo(fleet.obj.position); + } + + void start(AI& ai, FleetAI& fleet) override { + uint mprior = MP_Background; + if(gameTime < 6.0 * 60.0) + mprior = MP_Critical; + else if(priority > MiP_Normal) + mprior = MP_Normal; + @move = cast(ai.movement).move(fleet.obj, region, mprior); + } + + void tick(AI& ai, FleetAI& fleet, double time) { + if(move.failed) + canceled = true; + if(move.completed) { + //We managed to scout this system + if(fleet.obj.region !is region) { + @move = cast(ai.movement).move(fleet.obj, region.position + random3d(400.0)); + return; + } + completed = true; + + //Detect any anomalies and put them into the scanning queue + //TODO: Detect newly created anomalies in systems we already have vision over? + if(region.anomalyCount != 0) { + auto@ list = region.getAnomalies(); + Object@ obj; + while(receive(list, obj)) { + Anomaly@ anom = cast(obj); + if(anom !is null) + cast(ai.scouting).recordAnomaly(anom); + } + } + } + } +}; + +final class ScanningMission : Mission { + Anomaly@ anomaly; + MoveOrder@ move; + + void save(Fleets& fleets, SaveFile& file) override { + file << anomaly; + fleets.movement.saveMoveOrder(file, move); + } + + void load(Fleets& fleets, SaveFile& file) override { + file >> anomaly; + @move = fleets.movement.loadMoveOrder(file); + } + + double getPerformWeight(AI& ai, FleetAI& fleet) { + if(fleet.fleetClass != FC_Scout) { + if(gameTime > ai.behavior.scoutAllTimer) + return 0.0; + } + return 1.0 / anomaly.position.distanceTo(fleet.obj.position); + } + + void start(AI& ai, FleetAI& fleet) override { + uint mprior = MP_Background; + if(priority > MiP_Normal) + mprior = MP_Normal; + @move = cast(ai.movement).move(fleet.obj, anomaly, mprior); + } + + void tick(AI& ai, FleetAI& fleet, double time) { + if(move !is null) { + if(move.failed) { + canceled = true; + return; + } + if(move.completed) + @move = null; + } + if(move is null) { + if(anomaly is null || !anomaly.valid) { + completed = true; + return; + } + + if(anomaly.getEmpireProgress(ai.empire) >= 1.f) { + uint choose = 0; + uint possibs = 0; + uint optCnt = anomaly.getOptionCount(); + for(uint i = 0; i < optCnt; ++i) { + if(anomaly.isOptionSafe[i]) { + possibs += 1; + if(randomd() < 1.0 / double(possibs)) + choose = i; + } + } + + if(!ai.behavior.forbidAnomalyChoice && possibs != 0) { + anomaly.choose(ai.empire, choose); + } + else { + completed = true; + } + } + else { + if(!fleet.obj.hasOrders) + fleet.obj.addScanOrder(anomaly); + } + } + } +}; + +class Scouting : AIComponent { + Fleets@ fleets; + Systems@ systems; + Designs@ designs; + Construction@ construction; + Movement@ movement; + Creeping@ creeping; + + DesignTarget@ scoutDesign; + + array queue; + array active; + + array anomalies; + array scanQueue; + array scanActive; + + array constructing; + + int exploreHops = 0; + bool buildScouts = true; + + void create() { + @fleets = cast(ai.fleets); + @systems = cast(ai.systems); + @designs = cast(ai.designs); + @construction = cast(ai.construction); + @movement = cast(ai.movement); + @creeping = cast(ai.creeping); + } + + void save(SaveFile& file) { + designs.saveDesign(file, scoutDesign); + file << exploreHops; + + uint cnt = queue.length; + file << cnt; + for(uint i = 0; i < cnt; ++i) + fleets.saveMission(file, queue[i]); + + cnt = active.length; + file << cnt; + for(uint i = 0; i < cnt; ++i) + fleets.saveMission(file, active[i]); + + cnt = anomalies.length; + file << cnt; + for(uint i = 0; i < cnt; ++i) + file << anomalies[i]; + + cnt = scanQueue.length; + file << cnt; + for(uint i = 0; i < cnt; ++i) + fleets.saveMission(file, scanQueue[i]); + + cnt = scanActive.length; + file << cnt; + for(uint i = 0; i < cnt; ++i) + fleets.saveMission(file, scanActive[i]); + + cnt = constructing.length; + file << cnt; + for(uint i = 0; i < cnt; ++i) + construction.saveConstruction(file, constructing[i]); + } + + void load(SaveFile& file) { + @scoutDesign = designs.loadDesign(file); + file >> exploreHops; + + uint cnt = 0; + file >> cnt; + for(uint i = 0; i < cnt; ++i) { + auto@ miss = cast(fleets.loadMission(file)); + if(miss !is null) + queue.insertLast(miss); + } + + file >> cnt; + for(uint i = 0; i < cnt; ++i) { + auto@ miss = cast(fleets.loadMission(file)); + if(miss !is null) + active.insertLast(miss); + } + + file >> cnt; + for(uint i = 0; i < cnt; ++i) { + Anomaly@ anom; + file >> anom; + if(anom !is null) + anomalies.insertLast(anom); + } + + file >> cnt; + for(uint i = 0; i < cnt; ++i) { + auto@ miss = cast(fleets.loadMission(file)); + if(miss !is null) + scanQueue.insertLast(miss); + } + + file >> cnt; + for(uint i = 0; i < cnt; ++i) { + auto@ miss = cast(fleets.loadMission(file)); + if(miss !is null) + scanActive.insertLast(miss); + } + + file >> cnt; + for(uint i = 0; i < cnt; ++i) { + auto@ cons = cast(construction.loadConstruction(file)); + if(cons !is null) + constructing.insertLast(cons); + } + } + + void start() { + @scoutDesign = DesignTarget(DP_Scout, 16); + scoutDesign.targetMaintenance = 40; + designs.design(scoutDesign); + } + + bool isScouting(Region@ region) { + for(uint i = 0, cnt = queue.length; i < cnt; ++i) { + if(queue[i].region is region) + return true; + } + for(uint i = 0, cnt = active.length; i < cnt; ++i) { + if(active[i].region is region) + return true; + } + return false; + } + + ScoutingMission@ scout(Region@ region, uint priority = MiP_High) { + for(uint i = 0, cnt = queue.length; i < cnt; ++i) { + if(queue[i].region is region) + return queue[i]; + } + + for(uint i = 0, cnt = active.length; i < cnt; ++i) { + if(active[i].region is region) + return active[i]; + } + + if(log) + ai.print("Queue scouting mission", region); + + ScoutingMission mission; + @mission.region = region; + mission.priority = priority; + fleets.register(mission); + + queue.insertLast(mission); + return mission; + } + + void recordAnomaly(Anomaly@ anom) { + for(uint i = 0, cnt = scanActive.length; i < cnt; ++i) { + if(scanActive[i].anomaly is anom) + return; + } + + if(anomalies.find(anom) == -1) + anomalies.insertLast(anom); + } + + ScanningMission@ scan(Anomaly& anomaly, uint priority = MiP_Normal) { + for(uint i = 0, cnt = scanActive.length; i < cnt; ++i) { + if(scanActive[i].anomaly is anomaly) + return scanActive[i]; + } + + if(log) + ai.print("Queue scanning mission on "+anomaly.name, anomaly.region); + + ScanningMission mission; + @mission.anomaly = anomaly; + mission.priority = priority; + fleets.register(mission); + anomalies.remove(anomaly); + + scanQueue.insertLast(mission); + return mission; + } + + void focusTick(double time) { + //Remove completed scouting missions + for(uint i = 0, cnt = active.length; i < cnt; ++i) { + if(active[i].completed || active[i].canceled) { + active.removeAt(i); + --i; --cnt; + } + } + + //Remove completed scanning missions + for(uint i = 0, cnt = scanActive.length; i < cnt; ++i) { + if(scanActive[i].completed || scanActive[i].canceled) { + scanActive.removeAt(i); + --i; --cnt; + } + } + + if(ai.behavior.forbidScouting) return; + + //Make sure we have enough scouts active and scouting + if(fleets.count(FC_Scout) + constructing.length < ai.behavior.scoutsActive && buildScouts) + constructing.insertLast(construction.buildFlagship(scoutDesign)); + for(uint i = 0, cnt = constructing.length; i < cnt; ++i) { + if(constructing[i].completed && constructing[i].completedAt + 30.0 < gameTime) { + constructing.removeAt(i); + --i; --cnt; + } + } + + //See if we can fill the scouting queue with something nice + uint scoutClass = FC_Scout; + if(gameTime < ai.behavior.scoutAllTimer) + scoutClass = FC_ALL; + bool haveIdle = fleets.haveIdle(scoutClass); + + //See if we should queue up a new anomaly scan + if(scanQueue.length == 0 && anomalies.length != 0 && scanActive.length < ai.behavior.maxScanningMissions && haveIdle && (!ai.behavior.prioritizeScoutOverScan || active.length > 0)) { + Anomaly@ best; + double bestDist = INFINITY; + for(uint i = 0, cnt = anomalies.length; i < cnt; ++i) { + auto@ anom = anomalies[i]; + if(anom is null || !anom.valid) { + anomalies.removeAt(i); + --i; --cnt; + continue; + } + if(creeping.isQuarantined(anom.region)) + continue; + + double d = fleets.closestIdleTo(scoutClass, anom.position); + if(d < bestDist) { + @best = anom; + bestDist = d; + } + } + + if(best !is null) + scan(best); + } + + //Scan anomalies in our scan queue + if(scanQueue.length != 0) { + auto@ mission = scanQueue[0]; + if(mission.anomaly is null || !mission.anomaly.valid) { + scanQueue.removeAt(0); + } + else { + auto@ flAI = fleets.performMission(mission); + if(flAI !is null) { + if(log) + ai.print("Perform scanning mission with "+flAI.obj.name, mission.anomaly.region); + + scanQueue.remove(mission); + scanActive.insertLast(mission); + } + } + } + + //TODO: In large maps we should probably devote scouts to scouting enemies even before the map is fully explored + if(queue.length == 0 && active.length < ai.behavior.maxScoutingMissions && haveIdle) { + //Explore systems from the inside out + if(exploreHops != -1) { + double bestDist = INFINITY; + bool remainingHops = false; + bool emptyHops = true; + Region@ best; + + for(uint i = 0, cnt = systems.all.length; i < cnt; ++i) { + auto@ sys = systems.all[i]; + + if(sys.hopDistance == exploreHops) + emptyHops = false; + + if(sys.explored || isScouting(sys.obj)) + continue; + + if(sys.hopDistance == exploreHops) + remainingHops = true; + + double d = fleets.closestIdleTo(scoutClass, sys.obj.position); + if(sys.hopDistance != exploreHops) + d *= pow(ai.behavior.exploreBorderWeight, double(sys.hopDistance - exploreHops)); + + if(d < bestDist) { + bestDist = d; + @best = sys.obj; + } + } + + if(best !is null) + scout(best, priority=MiP_Normal); + + if(emptyHops) + exploreHops = -1; + else if(!remainingHops) + exploreHops += 1; + } + else { + //Gain vision over systems we haven't recently seen + Region@ best; + double bestWeight = 0; + double curTime = gameTime; + + for(uint i = 0, cnt = systems.all.length; i < cnt; ++i) { + auto@ sys = systems.all[i]; + if(sys.visible || sys.visibleNow(ai)) + continue; + if(isScouting(sys.obj)) + continue; + + double timer = curTime - sys.lastVisible; + if(timer < ai.behavior.minScoutingInterval) + continue; + + double w = 1.0; + w *= timer / ai.behavior.minScoutingInterval; + w /= fleets.closestIdleTo(scoutClass, sys.obj.position); + + if(!sys.explored) + w *= 10.0; + if(sys.seenPresent & ~ai.visionMask != 0) + w *= 2.0; + if(sys.seenPresent & ai.enemyMask != 0) { + if(sys.hopDistance < 2) + w *= 4.0; + w *= 4.0; + } + + if(w > bestWeight) { + bestWeight = w; + @best = sys.obj; + } + } + + if(best !is null) + scout(best, priority=MiP_Normal); + } + } + + //Try to find a scout to perform our top scouting mission from the queue + if(queue.length != 0) { + auto@ mission = queue[0]; + auto@ flAI = fleets.performMission(mission); + if(flAI !is null) { + if(log) + ai.print("Perform scouting mission with "+flAI.obj.name, mission.region); + + active.insertLast(mission); + queue.removeAt(0); + } + } + } +}; + +AIComponent@ createScouting() { + return Scouting(); +} ADDED scripts/server/empire_ai/weasel/Systems.as Index: scripts/server/empire_ai/weasel/Systems.as ================================================================== --- scripts/server/empire_ai/weasel/Systems.as +++ scripts/server/empire_ai/weasel/Systems.as @@ -0,0 +1,606 @@ +import empire_ai.weasel.WeaselAI; + +import empire_ai.weasel.Events; + +import empire_ai.weasel.searches; + +import ai.events; + +import systems; +import system_pathing; + +final class SystemAI { + const SystemDesc@ desc; + Region@ obj; + + double prevTick = 0.0; + + array planets; + array pickups; + array pickupProtectors; + array artifacts; + array asteroids; + + bool explored = false; + bool owned = false; + bool visible = false; + + int hopDistance = 0; + bool visited = false; + + bool border = false; + bool bordersEmpires = false; + bool outsideBorder = false; + + double lastVisible = 0; + uint seenPresent = 0; + + double focusDuration = 0; + + double enemyStrength = 0; + double lastStrengthCheck = 0; + + double nextDetailed = 0; + + SystemAI() { + } + + SystemAI(const SystemDesc@ sys) { + @desc = sys; + @obj = desc.object; + } + + void save(SaveFile& file) { + file << obj; + file << prevTick; + + uint cnt = planets.length; + file << cnt; + for(uint i = 0; i < cnt; ++i) + file << planets[i]; + + cnt = pickups.length; + file << cnt; + for(uint i = 0; i < cnt; ++i) + file << pickups[i]; + + cnt = pickupProtectors.length; + file << cnt; + for(uint i = 0; i < cnt; ++i) + file << pickupProtectors[i]; + + cnt = artifacts.length; + file << cnt; + for(uint i = 0; i < cnt; ++i) + file << artifacts[i]; + + cnt = asteroids.length; + file << cnt; + for(uint i = 0; i < cnt; ++i) + file << asteroids[i]; + + file << explored; + file << owned; + file << visible; + file << hopDistance; + file << border; + file << bordersEmpires; + file << outsideBorder; + file << lastVisible; + file << seenPresent; + file << enemyStrength; + file << lastStrengthCheck; + } + + void load(SaveFile& file) { + file >> obj; + file >> prevTick; + + uint cnt = 0; + file >> cnt; + planets.length = cnt; + for(uint i = 0; i < cnt; ++i) + file >> planets[i]; + + file >> cnt; + pickups.length = cnt; + for(uint i = 0; i < cnt; ++i) + file >> pickups[i]; + + file >> cnt; + pickupProtectors.length = cnt; + for(uint i = 0; i < cnt; ++i) + file >> pickupProtectors[i]; + + file >> cnt; + for(uint i = 0; i < cnt; ++i) { + Artifact@ artif; + file >> artif; + if(artif !is null) + artifacts.insertLast(artif); + } + + file >> cnt; + for(uint i = 0; i < cnt; ++i) { + Asteroid@ roid; + file >> roid; + if(roid !is null) + asteroids.insertLast(roid); + } + + file >> explored; + file >> owned; + file >> visible; + file >> hopDistance; + file >> border; + file >> bordersEmpires; + file >> outsideBorder; + file >> lastVisible; + file >> seenPresent; + file >> enemyStrength; + file >> lastStrengthCheck; + } + + bool visibleNow(AI& ai) { + return obj.VisionMask & ai.visionMask != 0; + } + + void strengthCheck(AI& ai, double minInterval = 30.0) { + if(lastStrengthCheck + minInterval > gameTime) + return; + if(!visible && lastVisible < gameTime - 30.0) + return; + lastStrengthCheck = gameTime; + enemyStrength = getTotalFleetStrength(obj, ai.enemyMask); + } + + void tick(AI& ai, Systems& systems, double time) { + //Check if we should be visible + bool shouldVisible = obj.VisionMask & ai.visionMask != 0; + if(visible != shouldVisible) { + if(visible) + lastVisible = gameTime; + visible = shouldVisible; + } + + //Check if we should be owned + bool shouldOwned = obj.TradeMask & ai.mask != 0; + if(owned != shouldOwned) { + if(shouldOwned) { + systems.owned.insertLast(this); + systems.hopsChanged = true; + hopDistance = 0; + systems.events.notifyOwnedSystemAdded(this, EventArgs()); + } + else { + hopDistance = 1; + systems.owned.remove(this); + systems.hopsChanged = true; + systems.events.notifyOwnedSystemRemoved(this, EventArgs()); + } + owned = shouldOwned; + } + + //Check if we should be border + bool shouldBorder = false; + bordersEmpires = false; + if(owned) { + for(uint i = 0, cnt = desc.adjacent.length; i < cnt; ++i) { + auto@ other = systems.getAI(desc.adjacent[i]); + if(other !is null && !other.owned) { + if(other.seenPresent & ~ai.teamMask != 0) + bordersEmpires = true; + shouldBorder = true; + break; + } + } + for(uint i = 0, cnt = desc.wormholes.length; i < cnt; ++i) { + auto@ other = systems.getAI(desc.wormholes[i]); + if(other !is null && !other.owned) { + if(other.seenPresent & ~ai.teamMask != 0) + bordersEmpires = true; + shouldBorder = true; + break; + } + } + } + + if(border != shouldBorder) { + if(shouldBorder) { + systems.border.insertLast(this); + systems.events.notifyBorderSystemAdded(this, EventArgs()); + } + else { + systems.border.remove(this); + systems.events.notifyBorderSystemRemoved(this, EventArgs()); + } + border = shouldBorder; + } + + //Check if we should be outsideBorder + bool shouldOutsideBorder = !owned && hopDistance == 1; + if(outsideBorder != shouldOutsideBorder) { + if(shouldOutsideBorder) { + systems.outsideBorder.insertLast(this); + systems.events.notifyOutsideBorderSystemAdded(this, EventArgs()); + } + else { + systems.outsideBorder.remove(this); + systems.events.notifyOutsideBorderSystemRemoved(this, EventArgs()); + } + outsideBorder = shouldOutsideBorder; + } + + //Check if we've been explored + if(visible && !explored) { + //Find all remnants in this system + auto@ objs = findType(obj, null, OT_Pickup); + for(uint i = 0, cnt = objs.length; i < cnt; ++i) { + Pickup@ p = cast(objs[i]); + if(p !is null) { + pickups.insertLast(p); + pickupProtectors.insertLast(p.getProtector()); + } + } + + explored = true; + } + + //Deal with recording new data on this system + if(explored) { + uint plCnt = obj.planetCount; + if(plCnt != planets.length) { + auto@ objs = findType(obj, null, OT_Planet); + planets.length = 0; + planets.reserve(objs.length); + for(uint i = 0, cnt = objs.length; i < cnt; ++i) { + Planet@ pl = cast(objs[i]); + if(pl !is null) + planets.insertLast(pl); + } + } + } + + if(visible) { + seenPresent = obj.PlanetsMask; + + uint astrCount = obj.asteroidCount; + if(astrCount != asteroids.length) { + auto@ objs = findType(obj, null, OT_Asteroid); + asteroids.length = 0; + asteroids.reserve(objs.length); + for(uint i = 0, cnt = objs.length; i < cnt; ++i) { + Asteroid@ a = cast(objs[i]); + if(a !is null) + asteroids.insertLast(a); + } + } + + for(uint i = 0, cnt = pickups.length; i < cnt; ++i) { + if(!pickups[i].valid) { + pickups.removeAt(i); + pickupProtectors.removeAt(i); + break; + } + } + + if(nextDetailed < gameTime) { + nextDetailed = gameTime + randomd(40.0, 100.0); + + auto@ objs = findType(obj, null, OT_Artifact); + artifacts.length = 0; + artifacts.reserve(objs.length); + for(uint i = 0, cnt = objs.length; i < cnt; ++i) { + Artifact@ a = cast(objs[i]); + if(a !is null) + artifacts.insertLast(a); + } + } + } + } +}; + +class Systems : AIComponent { + Events@ events; + + //All owned systems + array owned; + + //All owned systems that are considered our empire's border + array border; + + //All systems just outside our border + array outsideBorder; + + //All systems + array all; + + //Systems that need to be processed soon + array bumped; + array focused; + + uint sysIdx = 0; + bool hopsChanged = false; + + void create() { + @events = cast(ai.events); + } + + void save(SaveFile& file) { + uint cnt = all.length; + file << cnt; + for(uint i = 0; i < cnt; ++i) + all[i].save(file); + + cnt = owned.length; + file << cnt; + for(uint i = 0; i < cnt; ++i) + saveAI(file, owned[i]); + + cnt = border.length; + file << cnt; + for(uint i = 0; i < cnt; ++i) + saveAI(file, border[i]); + + cnt = outsideBorder.length; + file << cnt; + for(uint i = 0; i < cnt; ++i) + saveAI(file, outsideBorder[i]); + } + + void load(SaveFile& file) { + uint cnt = 0; + file >> cnt; + all.length = max(all.length, cnt); + for(uint i = 0; i < cnt; ++i) { + if(all[i] is null) + @all[i] = SystemAI(); + all[i].load(file); + } + + file >> cnt; + for(uint i = 0; i < cnt; ++i) { + auto@ data = loadAI(file); + if(data !is null) + owned.insertLast(data); + } + + file >> cnt; + for(uint i = 0; i < cnt; ++i) { + auto@ data = loadAI(file); + if(data !is null) + border.insertLast(data); + } + + file >> cnt; + for(uint i = 0; i < cnt; ++i) { + auto@ data = loadAI(file); + if(data !is null) + outsideBorder.insertLast(data); + } + } + + void loadFinalize(AI& ai) override { + for(uint i = 0, cnt = all.length; i < cnt; ++i) { + auto@ sys = getSystem(i); + @all[i].desc = sys; + @all[i].obj = sys.object; + } + } + + void saveAI(SaveFile& file, SystemAI@ ai) { + Region@ reg; + if(ai !is null) + @reg = ai.obj; + file << reg; + } + + SystemAI@ loadAI(SaveFile& file) { + Region@ reg; + file >> reg; + + if(reg is null) + return null; + + uint id = reg.SystemId; + if(id >= all.length) { + all.length = id+1; + @all[id] = SystemAI(); + @all[id].obj = reg; + } + + return all[id]; + } + + void focusTick(double time) { + if(all.length != systemCount) { + uint prevCount = all.length; + all.length = systemCount; + for(uint i = prevCount, cnt = all.length; i < cnt; ++i) + @all[i] = SystemAI(getSystem(i)); + } + if(hopsChanged) + calculateHops(); + } + + void tick(double time) override { + double curTime = gameTime; + + if(all.length != 0) { + uint tcount = max(ceil(time / 0.2), double(all.length)/20.0); + for(uint n = 0; n < tcount; ++n) { + sysIdx = (sysIdx+1) % all.length; + + auto@ sys = all[sysIdx]; + sys.tick(ai, this, curTime - sys.prevTick); + sys.prevTick = curTime; + } + } + + for(uint i = 0, cnt = bumped.length; i < cnt; ++i) { + auto@ sys = bumped[i]; + double tickTime = curTime - sys.prevTick; + if(tickTime != 0) { + sys.tick(ai, this, tickTime); + sys.prevTick = curTime; + } + } + bumped.length = 0; + + for(uint i = 0, cnt = focused.length; i < cnt; ++i) { + auto@ sys = focused[i]; + sys.focusDuration -= time; + + double tickTime = curTime - sys.prevTick; + if(tickTime != 0) { + sys.tick(ai, this, tickTime); + sys.prevTick = curTime; + } + + if(sys.focusDuration <= 0) { + focused.removeAt(i); + --i; --cnt; + } + } + } + + void calculateHops() { + if(!hopsChanged) + return; + hopsChanged = false; + priority_queue q; + for(uint i = 0, cnt = all.length; i < cnt; ++i) { + auto@ sys = all[i]; + sys.visited = false; + if(sys.owned) { + sys.hopDistance = 0; + q.push(int(i), 0); + } + else + sys.hopDistance = INT_MAX; + } + + while(!q.empty()) { + uint index = uint(q.top()); + q.pop(); + + auto@ sys = all[index]; + if(sys.visited) + continue; + + int dist = sys.hopDistance; + sys.visited = true; + + for(uint i = 0, cnt = sys.desc.adjacent.length; i < cnt; ++i) { + uint otherInd = sys.desc.adjacent[i]; + if(otherInd < all.length) { + auto@ other = all[otherInd]; + if(other.hopDistance > dist+1) { + other.hopDistance = dist+1; + q.push(otherInd, -other.hopDistance); + } + } + } + for(uint i = 0, cnt = sys.desc.wormholes.length; i < cnt; ++i) { + uint otherInd = sys.desc.wormholes[i]; + if(otherInd < all.length) { + auto@ other = all[otherInd]; + if(other.hopDistance > dist+1) { + other.hopDistance = dist+1; + q.push(otherInd, -other.hopDistance); + } + } + } + } + } + + void focus(Region@ reg, double duration = 30.0) { + bool found = false; + for(uint i = 0, cnt = focused.length; i < cnt; ++i) { + if(focused[i].obj is reg) { + focused[i].focusDuration = max(focused[i].focusDuration, duration); + found = true; + break; + } + } + + if(!found) { + auto@ sys = getAI(reg); + if(sys !is null) { + sys.focusDuration = duration; + focused.insertLast(sys); + } + } + } + + void bump(Region@ sys) { + if(sys !is null) + bump(getAI(sys)); + } + + void bump(SystemAI@ sys) { + if(sys !is null) + bumped.insertLast(sys); + } + + SystemAI@ getAI(uint idx) { + if(idx < all.length) + return all[idx]; + return null; + } + + SystemAI@ getAI(Region@ region) { + if(region is null) + return null; + uint idx = region.SystemId; + if(idx < all.length) + return all[idx]; + return null; + } + + SystemPath pather; + int hopDistance(Region& fromRegion, Region& toRegion){ + pather.generate(getSystem(fromRegion), getSystem(toRegion), keepCache=true); + if(!pather.valid) + return INT_MAX; + return pather.pathSize - 1; + } + + TradePath tradePather; + int tradeDistance(Region& fromRegion, Region& toRegion) { + @tradePather.forEmpire = ai.empire; + tradePather.generate(getSystem(fromRegion), getSystem(toRegion), keepCache=true); + if(!tradePather.valid) + return -1; + return tradePather.pathSize - 1; + } + + bool canTrade(Region& fromRegion, Region& toRegion) { + if(fromRegion.sharesTerritory(ai.empire, toRegion)) + return true; + int dist = tradeDistance(fromRegion, toRegion); + if(dist < 0) + return false; + return true; + } + + SystemAI@ getAI(const string& name) { + for(uint i = 0, cnt = all.length; i < cnt; ++i) { + if(all[i].obj.name.equals_nocase(name)) + return all[i]; + } + return null; + } + + uint index(const string& name) { + for(uint i = 0, cnt = all.length; i < cnt; ++i) { + if(all[i].obj.name.equals_nocase(name)) + return i; + } + return uint(-1); + } +}; + +AIComponent@ createSystems() { + return Systems(); +} ADDED scripts/server/empire_ai/weasel/War.as Index: scripts/server/empire_ai/weasel/War.as ================================================================== --- scripts/server/empire_ai/weasel/War.as +++ scripts/server/empire_ai/weasel/War.as @@ -0,0 +1,905 @@ +// War +// --- +// Attacks and defends from enemy attacks during situations of war. +// + +import empire_ai.weasel.WeaselAI; +import empire_ai.weasel.Intelligence; +import empire_ai.weasel.Relations; +import empire_ai.weasel.Fleets; +import empire_ai.weasel.Systems; +import empire_ai.weasel.Movement; +import empire_ai.weasel.Scouting; +import empire_ai.weasel.Military; +import empire_ai.weasel.searches; + +import regions.regions; + +class BattleMission : Mission { + Region@ battleIn; + FleetAI@ fleet; + MoveOrder@ move; + Object@ defending; + Planet@ capturing; + Empire@ captureFrom; + bool arrived = false; + + void save(Fleets& fleets, SaveFile& file) override { + file << battleIn; + fleets.saveAI(file, fleet); + fleets.movement.saveMoveOrder(file, move); + file << defending; + file << capturing; + file << captureFrom; + file << arrived; + } + + void load(Fleets& fleets, SaveFile& file) override { + file >> battleIn; + @fleet = fleets.loadAI(file); + @move = fleets.movement.loadMoveOrder(file); + file >> defending; + file >> capturing; + file >> captureFrom; + file >> arrived; + } +}; + +double captureSupply(Empire& emp, Object& check) { + double loy = check.getLoyaltyFacing(emp); + double cost = config::SIEGE_LOYALTY_SUPPLY_COST * loy; + cost *= emp.CaptureSupplyFactor; + cost *= check.owner.CaptureSupplyDifficulty; + return cost; +} + +class Battle { + SystemAI@ system; + Region@ staging; + array fleets; + uint curPriority = MiP_Critical; + bool isAttack = false; + + double enemyStrength; + double ourStrength; + double lastCombat = 0; + double bestCapturePct; + double lastHadFleets = 0; + bool inCombat = false; + bool isUnderSiege = false; + + Planet@ defendPlanet; + Object@ eliminate; + + Battle() { + lastHadFleets = gameTime; + lastCombat = gameTime; + } + + void save(War& war, SaveFile& file) { + war.systems.saveAI(file, system); + + uint cnt = fleets.length; + file << cnt; + for(uint i = 0; i < cnt; ++i) + war.fleets.saveMission(file, fleets[i]); + + file << curPriority; + file << isAttack; + file << lastCombat; + file << inCombat; + file << defendPlanet; + file << eliminate; + file << isUnderSiege; + file << bestCapturePct; + } + + void load(War& war, SaveFile& file) { + @system = war.systems.loadAI(file); + + uint cnt = 0; + file >> cnt; + for(uint i = 0; i < cnt; ++i) { + auto@ miss = cast(war.fleets.loadMission(file)); + if(miss !is null) + fleets.insertLast(miss); + } + + file >> curPriority; + file >> isAttack; + file >> lastCombat; + file >> inCombat; + file >> defendPlanet; + file >> eliminate; + file >> isUnderSiege; + file >> bestCapturePct; + } + + BattleMission@ join(AI& ai, War& war, FleetAI@ flAI) { + BattleMission mission; + @mission.fleet = flAI; + @mission.battleIn = system.obj; + mission.priority = curPriority; + + Object@ moveTo = system.obj; + if(defendPlanet !is null) + @moveTo = defendPlanet; + else if(eliminate !is null && eliminate.isShip) + @moveTo = eliminate; + @mission.move = war.movement.move(flAI.obj, moveTo, MP_Critical, spread=true, nearOnly=true); + + //Station this fleet nearby after the battle is over + if(staging !is null) + war.military.stationFleet(flAI, staging); + + if(war.log) + ai.print("Assign to battle at "+system.obj.name + +" for strength "+standardize(ourStrength * 0.001, true) + + " vs their "+standardize(enemyStrength * 0.001, true), flAI.obj); + + fleets.insertLast(mission); + war.fleets.performMission(flAI, mission); + return mission; + } + + bool stayingHere(Object@ other) { + if(other is null || !other.hasMover) + return true; + if(!inRegion(system.obj, other.position)) + return false; + double acc = other.maxAcceleration; + if(acc <= 0.0001) + return true; + vec3d compDest = other.computedDestination; + if(inRegion(system.obj, compDest)) + return true; + if(inRegion(system.obj, other.position + other.velocity * 10.0)) + return true; + return false; + } + + bool tick(AI& ai, War& war, double time) { + //Compute strength values + enemyStrength = getTotalFleetStrength(system.obj, ai.enemyMask, planets=true); + ourStrength = 0; + for(uint i = 0, cnt = fleets.length; i < cnt; ++i) + ourStrength += sqrt(fleets[i].fleet.strength); + ourStrength *= ourStrength; + inCombat = false; + bool ourPlanetsPresent = system.obj.PlanetsMask & (ai.allyMask | ai.mask) != 0; + + if((enemyStrength < 0.01 || !ourPlanetsPresent) && defendPlanet is null) + isUnderSiege = false; + + //Remove lost fleets + bool anyArrived = false; + bestCapturePct = 0.0; + for(uint i = 0, cnt = fleets.length; i < cnt; ++i) { + auto@ miss = fleets[i]; + miss.priority = curPriority; + if(!miss.fleet.obj.valid || miss.canceled) { + if(!miss.fleet.obj.valid) { + for(uint i = 0, cnt = getEmpireCount(); i < cnt; ++i) { + Empire@ other = getEmpire(i); + if(!other.major || !ai.empire.isHostile(other)) + continue; + if(system.obj.ContestedMask & other.mask != 0) + war.relations.recordLostTo(other, miss.fleet.obj); + } + } + miss.canceled = true; + if(war.log) + ai.print("BATTLE: lost fleet "+miss.fleet.obj.name, system.obj); + fleets.removeAt(i); + --i; --cnt; + if(fleets.length == 0) + lastHadFleets = gameTime; + continue; + } + if(miss.move !is null) { + if(miss.move.failed) { + miss.canceled = true; + if(war.log) + ai.print("BATTLE: move failed on lost fleet "+miss.fleet.obj.name, system.obj); + fleets.removeAt(i); + --i; --cnt; + if(fleets.length == 0) + lastHadFleets = gameTime; + continue; + } + if(miss.move.completed) { + miss.arrived = true; + @miss.move = null; + } + } + if(miss.arrived) { + anyArrived = true; + + bool shouldRetreat = false; + if(miss.fleet.supplies < 0.05) { + if(isCapturingAny && eliminate is null) + shouldRetreat = true; + else if(ourStrength < enemyStrength * 0.75) + shouldRetreat = true; + } + if(miss.fleet.fleetHealth < 0.25) { + if(ourStrength < enemyStrength * 0.5) + shouldRetreat = true; + } + //DOF - Adding some leader based checks + if(miss.fleet.flagshipHealth < 0.5) { + if(ourStrength < enemyStrength * 0.75) + shouldRetreat = true; + } + if(shouldRetreat) { + war.fleets.returnToBase(miss.fleet); + fleets.removeAt(i); + miss.canceled = true; + --i; --cnt; + if(fleets.length == 0) + lastHadFleets = gameTime; + continue; + } + } + if(miss.capturing !is null) + bestCapturePct = max(bestCapturePct, miss.capturing.capturePct); + } + + //Defend our planets + if(defendPlanet is null) { + Planet@ defPl; + double bestWeight = 0.0; + for(uint i = 0, cnt = system.planets.length; i < cnt; ++i) { + Planet@ pl = system.planets[i]; + double w = 1.0; + if(pl.owner is ai.empire) + w *= 2.0; + else if(pl.owner.mask & ai.allyMask != 0) + w *= 0.5; + else + continue; + double capt = pl.capturePct; + if(capt <= 0.01) + continue; + w *= capt; + + if(!pl.enemiesInOrbit) + continue; + if(w > bestWeight) { + bestWeight = w; + @defPl = pl; + } + } + + if(defPl !is null) { + if(war.log) + ai.print("BATTLE: protect planet "+defPl.name, system.obj); + + @defendPlanet = defPl; + for(uint i = 0, cnt = fleets.length; i < cnt; ++i) + moveTo(fleets[i], defendPlanet, force=true); + } + } + else { + //Check if there are still enemies in orbit + if(!defendPlanet.enemiesInOrbit || !defendPlanet.valid || defendPlanet.owner.isHostile(ai.empire)) + @defendPlanet = null; + } + if(defendPlanet !is null) { + //Make sure one fleet is in orbit + inCombat = true; + isUnderSiege = true; + for(uint i = 0, cnt = fleets.length; i < cnt; ++i) + moveTo(fleets[i], defendPlanet); + } + + //Eliminate any remaining threats + if(!inCombat) { + //Eliminate any hostile targets in the system + if(eliminate !is null) { + //Make sure this is still a valid target to eliminate + bool valid = true; + if(!eliminate.valid) { + valid = false; + war.relations.recordTakenFrom(eliminate.owner, eliminate); + } + else if(!stayingHere(eliminate)) + valid = false; + else if(!eliminate.isVisibleTo(ai.empire)) + valid = false; + else if(!ai.empire.isHostile(eliminate.owner)) + valid = false; + + if(!valid) { + @eliminate = null; + clearOrders(); + } + else { + for(uint i = 0, cnt = fleets.length; i < cnt; ++i) + attack(fleets[i], eliminate); + inCombat = true; + } + } + else { + //Find a new target to eliminate + Object@ check = findEnemy(system.obj, ai.empire, ai.enemyMask); + if(check !is null) { + if(stayingHere(check)) { + @eliminate = check; + for(uint i = 0, cnt = fleets.length; i < cnt; ++i) { + auto@ obj = fleets[i].fleet.obj; + if(!fleets[i].arrived) + continue; + obj.addAttackOrder(eliminate); + } + + if(war.log) + ai.print("BATTLE: Eliminate "+eliminate.name, system.obj); + + inCombat = true; + for(uint i = 0, cnt = fleets.length; i < cnt; ++i) + attack(fleets[i], eliminate, force=true); + } + } + } + } + else { + @eliminate = null; + } + + //Capture enemy planets if possible + //TODO: Respond to defense by abandoning all but 1 capture and swarming around the best one + if(!inCombat) { + for(uint i = 0, cnt = fleets.length; i < cnt; ++i) { + auto@ miss = fleets[i]; + if(miss.capturing !is null) { + if(canCapture(ai, miss, miss.capturing) && miss.fleet.remainingSupplies > captureSupply(ai.empire, miss.capturing) && miss.fleet.obj.hasOrders) { + inCombat = true; + continue; + } + else { + if(miss.capturing.owner is ai.empire && miss.captureFrom !is null) + war.relations.recordTakenFrom(miss.captureFrom, miss.capturing); + @miss.capturing = null; + @miss.captureFrom = null; + } + } + if(!miss.arrived) + continue; + + Planet@ bestCapture; + double totalWeight = 0; + + for(uint i = 0, cnt = system.planets.length; i < cnt; ++i) { + Planet@ check = system.planets[i]; + if(!canCapture(ai, miss, check)) + continue; + + //Don't send two fleets to the same thing + if(isCapturing(check)) + continue; + + //Make sure we have the supplies remaining to capture + if(miss.fleet.remainingSupplies < captureSupply(ai.empire, check) * ai.behavior.captureSupplyEstimate) + continue; + + double str = check.getFleetStrength(); + double w = 1.0; + w *= check.getLoyaltyFacing(ai.empire); + if(str != 0) + w /= str; + + totalWeight += w; + if(randomd() < w / totalWeight) + @bestCapture = check; + } + + if(bestCapture !is null) { + if(war.log) + ai.print("BATTLE: Capture "+bestCapture.name+" with "+miss.fleet.obj.name, system.obj); + + @miss.capturing = bestCapture; + @miss.captureFrom = bestCapture.owner; + miss.fleet.obj.addCaptureOrder(bestCapture); + inCombat = true; + } + } + } + else { + for(uint i = 0, cnt = fleets.length; i < cnt; ++i) { + auto@ miss = fleets[i]; + @miss.capturing = null; + @miss.captureFrom = null; + } + } + + //Keep fleets here in non-critical mode for a few minutes + if(!inCombat && (anyArrived || !isAttack)) { + //TODO: Don't start this countdown until we've actually arrived + if(gameTime > lastCombat + 90.0) { + for(uint i = 0, cnt = fleets.length; i < cnt; ++i) + fleets[i].completed = true; + if(war.log) + ai.print("BATTLE: ended", system.obj); + return false; + } + else if(gameTime > lastCombat + 30.0) { + curPriority = MiP_Normal; + } + else { + curPriority = MiP_High; + } + } + else { + if(ourPlanetsPresent && isUnderSiege) { + curPriority = MiP_Critical; + if(war.winnability(this) < 0.5) + curPriority = MiP_High; + } + else if(bestCapturePct > 0.75) + curPriority = MiP_Critical; + else + curPriority = MiP_High; + lastCombat = gameTime; + } + + //If needed, claim fleets + if(ourStrength < enemyStrength * ai.behavior.battleStrengthOverkill) { + FleetAI@ claim; + double bestWeight = 0; + + for(uint i = 0, cnt = war.fleets.fleets.length; i < cnt; ++i) { + auto@ fleet = war.fleets.fleets[i]; + double w = war.assignable(this, fleet); + + if(w > bestWeight) { + bestWeight = w; + @claim = fleet; + } + } + + if(claim !is null) + join(ai, war, claim); + } + + //Give up the battle when we should + if(fleets.length == 0) { + if(!ourPlanetsPresent && !isAttack) { + //We lost all our planets before we could respond with anything. + // We might be able to use an attack to claim it back later, but for now we just give up on it. + if(war.log) + ai.print("BATTLE: aborted defense, no fleets and no planets left", system.obj); + for(uint i = 0, cnt = fleets.length; i < cnt; ++i) + fleets[i].canceled = true; + return false; + } + if(isAttack) { + //We haven't been able to find any fleets to assign here for a while, + //so just abort the attack + if(gameTime - lastHadFleets > 60.0) { + if(war.log) + ai.print("BATTLE: aborted attack, no fleets available", system.obj); + for(uint i = 0, cnt = fleets.length; i < cnt; ++i) + fleets[i].canceled = true; + return false; + } + } + } + + return true; + } + + void clearOrders() { + for(uint i = 0, cnt = fleets.length; i < cnt; ++i) { + auto@ obj = fleets[i].fleet.obj; + if(!fleets[i].arrived) + continue; + if(obj.hasOrders) + obj.clearOrders(); + } + } + + bool canCapture(AI& ai, BattleMission@ miss, Planet@ check) { + if(!ai.empire.isHostile(check.owner)) + return false; + //TODO: Wait around a while maybe? + if(check.isProtected(ai.empire)) + return false; + return true; + } + + void moveTo(BattleMission@ miss, Planet@ defPl, bool force = false) { + if(!miss.arrived) + return; + if(!force) { + if(miss.fleet.obj.hasOrders) + return; + double dist = miss.fleet.obj.position.distanceTo(defPl.position); + if(dist < defPl.OrbitSize) + return; + } + vec3d pos = defPl.position; + vec2d offset = random2d(defPl.OrbitSize * 0.85); + pos.x += offset.x; + pos.z += offset.y; + miss.fleet.obj.addMoveOrder(pos); + } + + void attack(BattleMission@ miss, Object@ target, bool force = false) { + //TODO: make this not chase stuff out of the system like a madman? + // (in attack logic as well) + if(!miss.arrived) + return; + if(!force) { + if(miss.fleet.obj.hasOrders) + return; + } + miss.fleet.obj.addAttackOrder(target); + } + + bool isCapturing(Planet@ pl) { + for(uint i = 0, cnt = fleets.length; i < cnt; ++i) { + if(fleets[i].capturing is pl) + return true; + } + return false; + } + + bool get_isCapturingAny() { + for(uint i = 0, cnt = fleets.length; i < cnt; ++i) { + if(fleets[i].capturing !is null) + return true; + } + return false; + } +}; + +class War : AIComponent { + Fleets@ fleets; + Intelligence@ intelligence; + Relations@ relations; + Movement@ movement; + Scouting@ scouting; + Systems@ systems; + Military@ military; + + array battles; + + ScoutingMission@ currentScout; + + void create() { + @fleets = cast(ai.fleets); + @intelligence = cast(ai.intelligence); + @relations = cast(ai.relations); + @movement = cast(ai.movement); + @scouting = cast(ai.scouting); + @systems = cast(ai.systems); + @military = cast(ai.military); + } + + void save(SaveFile& file) { + uint cnt = battles.length; + file << cnt; + for(uint i = 0; i < cnt; ++i) + battles[i].save(this, file); + + fleets.saveMission(file, currentScout); + } + + void load(SaveFile& file) { + uint cnt = 0; + file >> cnt; + battles.length = cnt; + for(uint i = 0; i < cnt; ++i) { + @battles[i] = Battle(); + battles[i].load(this, file); + } + + @currentScout = cast(fleets.loadMission(file)); + ai.behavior.remnantAllowArbitraryClear = false; + } + + double getCombatReadyStrength() { + double str = 0; + for(uint i = 0, cnt = fleets.fleets.length; i < cnt; ++i) { + auto@ flAI = fleets.fleets[i]; + if(flAI.fleetClass != FC_Combat) + continue; + if(!flAI.readyForAction) + continue; + str += flAI.strength; + } + return str * str; + } + + Battle@ attack(SystemAI@ sys) { + Battle atk; + @atk.system = sys; + atk.isAttack = true; + atk.curPriority = MiP_High; + @atk.staging = military.getStagingFor(sys.obj); + + if(log) + ai.print("Organizing an attack against "+sys.obj.name); + + claimFleetsFor(atk); + battles.insertLast(atk); + return atk; + } + + Battle@ defend(SystemAI@ sys) { + Battle def; + @def.system = sys; + @def.staging = military.getClosestStaging(sys.obj); + + if(log) + ai.print("Organizing defense for "+sys.obj.name); + + battles.insertLast(def); + return def; + } + + void claimFleetsFor(Battle@ atk) { + //TODO: This currently claims everything not in use, should it + //leave some reserves for defense? Is that good? + for(uint i = 0, cnt = fleets.fleets.length; i < cnt; ++i) { + auto@ flAI = fleets.fleets[i]; + if(flAI.fleetClass != FC_Combat) + continue; + if(!flAI.readyForAction) + continue; + atk.join(ai, this, flAI); + } + } + + void sendFleetToJoin(Battle@ atk) { + for(uint i = 0, cnt = fleets.fleets.length; i < cnt; ++i) { + auto@ flAI = fleets.fleets[i]; + if(flAI.fleetClass != FC_Combat) + continue; + if(!flAI.readyForAction) + continue; + atk.join(ai, this, flAI); + break; + } + } + + bool isFightingIn(Region@ reg) { + for(uint i = 0, cnt = battles.length; i < cnt; ++i) { + if(battles[i].system.obj is reg) + return true; + } + return false; + } + + void tick(double time) override { + for(uint i = 0, cnt = battles.length; i < cnt; ++i) { + if(!battles[i].tick(ai, this, time)) { + battles.removeAt(i); + --i; --cnt; + } + } + } + + double ourStrength; + double curTime; + SystemAI@ best; + double totalWeight; + SystemAI@ scout; + uint scoutCount; + void check(SystemAI@ sys, double baseWeight) { + if(isFightingIn(sys.obj)) + return; + + if(!sys.visible) { + sys.strengthCheck(ai, minInterval=5*60.0); + if(sys.lastStrengthCheck < curTime - 5 * 60.0) { + scoutCount += 1; + if(randomd() < 1.0 / double(scoutCount)) + @scout = sys; + return; + } + } + else { + sys.strengthCheck(ai, minInterval=60.0); + } + + double theirStrength = sys.enemyStrength; + if(ourStrength < theirStrength * ai.behavior.attackStrengthOverkill) + return; + + double w = baseWeight; + + //Try to capture less important systems at first + //TODO: This should flip when we go from border skirmishes to subjugation war + uint capturable = 0; + for(uint i = 0, cnt = sys.planets.length; i < cnt; ++i) { + Planet@ pl = sys.planets[i]; + if(!ai.empire.isHostile(pl.owner)) + continue; + if(pl.isProtected(ai.empire)) + continue; + w /= 1.0 + double(pl.level); + capturable += 1; + } + + //Ignore protected systems + if(capturable == 0) + return; + + //See where their defenses are low + if(theirStrength != 0) { + double defRatio = ourStrength / theirStrength; + if(defRatio > 4.0) { + //We prefer destroying some minor assets over fighting an entirely undefended system, + //because it hurts more to lose stuff. + w *= 6.0; + } + } + else { + w *= 2.0; + } + + totalWeight += w; + if(randomd() < w / totalWeight) + @best = sys; + } + + int totalEnemySize(SystemAI@ sys) { + int size = 0; + for(uint i = 0, cnt = getEmpireCount(); i < cnt; ++i) { + Empire@ other = getEmpire(i); + if(other.major && ai.empire.isHostile(other)) + size += sys.obj.getStrength(other); + } + return size; + } + + bool isUnderAttack(SystemAI@ sys) { + if(sys.obj.ContestedMask & ai.mask == 0) + return false; + if(totalEnemySize(sys) < 100) { + if(sys.obj.SiegedMask & ai.mask == 0) + return false; + } + return true; + } + + double assignable(Battle& battle, FleetAI& fleet) { + if(fleet.fleetClass != FC_Combat) + return 0.0; + double w = 1.0; + if(fleet.mission !is null) { + w *= 0.1; + if(fleet.mission.priority >= MiP_High) + w *= 0.1; + if(fleet.mission.priority >= battle.curPriority) + return 0.0; + + auto@ miss = cast(fleet.mission); + if(miss !is null && miss.battleIn is battle.system.obj) + return 0.0; + } + else if(fleet.isHome && fleet.stationed is battle.system.obj) { + //This should be allowed to fight always + } + else if(battle.curPriority >= MiP_Critical) { + if(fleet.supplies < 0.25) + return 0.0; + if(fleet.fleetHealth < 0.25) + return 0.0; + if(fleet.filled < 0.2) + return 0.0; + //DOF - Do not send badly damaged flagships + if(fleet.flagshipHealth < 0.5) + return 0.0; + + if(fleet.obj.isMoving) { + if(fleet.obj.velocity.length / fleet.obj.maxAcceleration > 16.0) + w *= 0.1; + } + } + else { + if(!fleet.readyForAction) + return 0.0; + } + double fleetStrength = fleet.strength; + if(battle.ourStrength + fleetStrength < battle.enemyStrength * ai.behavior.battleStrengthOverkill) + w *= 0.25; + return w; + } + + double winnability(Battle& battle) { + double ours = sqrt(battle.ourStrength); + double theirs = battle.enemyStrength; + if(theirs <= 0.0) + return 10.0; + + for(uint i = 0, cnt = fleets.fleets.length; i < cnt; ++i) { + auto@ fleet = fleets.fleets[i]; + double w = assignable(battle, fleet); + if(w != 0.0) + ours += sqrt(fleet.strength); + } + ours *= ours; + + return ours / theirs; + } + + void focusTick(double time) override { + if(currentScout !is null) { + if(currentScout.canceled || currentScout.completed) + @currentScout = null; + } + + //Change our behavior a little depending on the state + ai.behavior.remnantAllowArbitraryClear = !relations.isFightingWar(aggressive=true) && battles.length == 0; + + //Find any systems that need defending + //TODO: Defend allies at lowered priority + if(ai.behavior.forbidDefense) { + for(uint i = 0, cnt = systems.owned.length; i < cnt; ++i) { + SystemAI@ sys = systems.owned[i]; + if(!isUnderAttack(sys)) + continue; + if(isFightingIn(sys.obj)) + continue; + defend(sys); + return; + } + } + + if(ai.behavior.forbidAttack) + return; + //Do attacks + uint ready = fleets.countCombatReadyFleets(); + if(ready != 0) { + //See if we can start a new attack + if(battles.length < ai.behavior.maxBattles && relations.isFightingWar(aggressive=true) + && (battles.length == 0 || ready > ai.behavior.battleReserveFleets)) { + //Determine our own strength + ourStrength = getCombatReadyStrength(); + + //Evaluate systems to attack in our aggressive war + @best = null; + totalWeight = 0; + curTime = gameTime; + @scout = null; + scoutCount = 0; + //TODO: Consider aggressive wars against an empire to also be against that empire's vassals + for(uint i = 0, cnt = intelligence.intel.length; i < cnt; ++i) { + auto@ intel = intelligence.intel[i]; + if(intel is null) + continue; + + auto@ relation = relations.get(intel.empire); + if(!relation.atWar || !relation.aggressive) + continue; + + for(uint n = 0, ncnt = intel.shared.length; n < ncnt; ++n) { + auto@ sys = intel.shared[n]; + check(sys, 20.0); + } + + for(uint n = 0, ncnt = intel.theirBorder.length; n < ncnt; ++n) { + auto@ sys = intel.theirBorder[n]; + check(sys, 1.0); + } + } + + //Make the attack with our fleets + if(best !is null) + attack(best); + else if(scout !is null && currentScout is null) { + if(log) + ai.print("War requests scout to flyby "+scout.obj.name); + @currentScout = scouting.scout(scout.obj); + } + } + } + } +}; + +AIComponent@ createWar() { + return War(); +} ADDED scripts/server/empire_ai/weasel/WeaselAI.as Index: scripts/server/empire_ai/weasel/WeaselAI.as ================================================================== --- scripts/server/empire_ai/weasel/WeaselAI.as +++ scripts/server/empire_ai/weasel/WeaselAI.as @@ -0,0 +1,799 @@ +import settings.game_settings; +from empire_ai.EmpireAI import AIController; + +import AIComponent@ createEvents() from "empire_ai.weasel.Events"; +import AIComponent@ createColonization() from "empire_ai.weasel.Colonization"; +import AIComponent@ createResources() from "empire_ai.weasel.Resources"; +import AIComponent@ createPlanets() from "empire_ai.weasel.Planets"; +import AIComponent@ createSystems() from "empire_ai.weasel.Systems"; +import AIComponent@ createFleets() from "empire_ai.weasel.Fleets"; +import AIComponent@ createScouting() from "empire_ai.weasel.Scouting"; +import AIComponent@ createDevelopment() from "empire_ai.weasel.Development"; +import AIComponent@ createDesigns() from "empire_ai.weasel.Designs"; +import AIComponent@ createBudget() from "empire_ai.weasel.Budget"; +import AIComponent@ createConstruction() from "empire_ai.weasel.Construction"; +import AIComponent@ createMilitary() from "empire_ai.weasel.Military"; +import AIComponent@ createMovement() from "empire_ai.weasel.Movement"; +import AIComponent@ createCreeping() from "empire_ai.weasel.Creeping"; +import AIComponent@ createRelations() from "empire_ai.weasel.Relations"; +import AIComponent@ createIntelligence() from "empire_ai.weasel.Intelligence"; +import AIComponent@ createWar() from "empire_ai.weasel.War"; +import AIComponent@ createResearch() from "empire_ai.weasel.Research"; +import AIComponent@ createEnergy() from "empire_ai.weasel.Energy"; +import IAIComponent@ createDiplomacy() from "empire_ai.weasel.Diplomacy"; +import AIComponent@ createConsider() from "empire_ai.weasel.Consider"; +import AIComponent@ createOrbitals() from "empire_ai.weasel.Orbitals"; +import AIComponent@ createInfrastructure() from "empire_ai.weasel.Infrastructure"; + +import AIComponent@ createHyperdrive() from "empire_ai.weasel.ftl.Hyperdrive"; +import AIComponent@ createGate() from "empire_ai.weasel.ftl.Gate"; +import AIComponent@ createFling() from "empire_ai.weasel.ftl.Fling"; +import AIComponent@ createSlipstream() from "empire_ai.weasel.ftl.Slipstream"; +import AIComponent@ createJumpdrive() from "empire_ai.weasel.ftl.Jumpdrive"; + +import AIComponent@ createVerdant() from "empire_ai.weasel.race.Verdant"; +import AIComponent@ createMechanoid() from "empire_ai.weasel.race.Mechanoid"; +import AIComponent@ createStarChildren() from "empire_ai.weasel.race.StarChildren"; +import AIComponent@ createExtragalactic() from "empire_ai.weasel.race.Extragalactic"; +import AIComponent@ createLinked() from "empire_ai.weasel.race.Linked"; +import AIComponent@ createDevout() from "empire_ai.weasel.race.Devout"; +import AIComponent@ createAncient() from "empire_ai.weasel.race.Ancient"; + +import AIComponent@ createInvasion() from "empire_ai.weasel.misc.Invasion"; +import bool hasInvasionMap() from "Invasion.InvasionMap"; + +from buildings import BuildingType; +from orbitals import OrbitalModule; +from constructions import ConstructionType; +import util.formatting; + +from empire import ai_full_speed; + +from traits import getTraitID; + +export IAIComponent, AIComponent, AI; +uint GUARD = 0xDEADBEEF; + +enum AddedComponent { + AC_Invasion = 0x1, +}; + +interface IAIComponent : Savable { + void set(AI& ai); + void setLog(); + void setLogCritical(); + + double getPrevFocus(); + void setPrevFocus(double value); + + void create(); + void start(); + + void tick(double time); + void focusTick(double time); + void turn(); + + void save(SaveFile& file); + void load(SaveFile& file); + void postLoad(AI& ai); + void postSave(AI& ai); + void loadFinalize(AI& ai); +}; + +class AIComponent : IAIComponent, Savable { + AI@ ai; + double prevFocus = 0; + bool log = false; + bool logCritical = false; + bool logErrors = true; + + double getPrevFocus() { return prevFocus; } + void setPrevFocus(double value) { prevFocus = value; } + + void setLog() { log = true; } + void setLogCritical() { logCritical = true; } + + void set(AI& ai) { @this.ai = ai; } + void create() {} + void start() {} + + void tick(double time) {} + void focusTick(double time) {} + void turn() {} + + void save(SaveFile& file) {} + void load(SaveFile& file) {} + void postLoad(AI& ai) {} + void postSave(AI& ai) {} + void loadFinalize(AI& ai) {} +}; + +class ProfileData { + double tickPeak = 0.0; + double tickAvg = 0.0; + double tickCount = 0.0; + + double focusPeak = 0.0; + double focusAvg = 0.0; + double focusCount = 0.0; +}; + +final class AIBehavior { + //AIEmpire controls + bool forbidDiplomacy = false; + bool forbidColonization = false; + bool forbidCreeping = false; + bool forbidResearch = false; + bool forbidDefense = false; + bool forbidAttack = false; + bool forbidConstruction = false; + bool forbidScouting = false; + bool forbidAnomalyChoice = false; + bool forbidArtifact = false; + bool forbidScuttle = false; + + //How many focuses we can manage in a tick + uint focusPerTick = 2; + + //The maximum colonizations this AI can do in one turn + uint maxColonizations = UINT_MAX; + //How many colonizations we're guaranteed to be able to do in one turn regardless of finances + uint guaranteeColonizations = 2; + //How many colonizations at most we can be doing at once + uint maxConcurrentColonizations = UINT_MAX; + //Whether this AI will colonize planets in systems owned by someone else + // TODO: This should be partially ignored for border systems, so it can try to aggressively expand into the border + bool colonizeEnemySystems = false; + bool colonizeNeutralOwnedSystems = false; + bool colonizeAllySystems = false; + //How much this AI values claiming new systems instead of colonizing stuff in its existing ones + double weightOutwardExpand = 2.0; + //How much money this AI considers a colonization event to cost out of the budget + int colonizeBudgetCost = 80; + //Whether to do any generic expansion beyond any requests + bool colonizeGenericExpand = true; + //Latest percentage into a budget cycle that we still allow colonization + double colonizeMaxBudgetProgress = 0.66; + //Time after initial ownership change that an incomplete colonization is canceled + double colonizeFailGraceTime = 100.0; + //Time a planet that we failed to colonize is disregarded for colonization + double colonizePenalizeTime = 9.0 * 60.0; + + //Maximum amount of scouting missions that can be performed simultaneously + uint maxScoutingMissions = UINT_MAX; + //Minimum time after losing vision over a system that we can scout it again + double minScoutingInterval = 3.0 * 60.0; + //Weight that it gives to exploring things near our empire instead of greedily exploring nearby things + double exploreBorderWeight = 2.0; + //How long we consider all fleets viable for scouting with + double scoutAllTimer = 3.0 * 60.0; + //How many scouts we want to have active + uint scoutsActive = 2; + //How many scanning missions we can do at once + uint maxScanningMissions = 1; + //Whether to prioritize scouting over scanning if we only have one scout + bool prioritizeScoutOverScan = true; + + //Weights for what to do in generic planet development + // Leveling up an existing development focus + double focusDevelopWeight = 1.0; + // Colonizing a new scalable or high tier to focus on + double focusColonizeNewWeight = 4.0; + // Colonizing a new high tier resource to import to one of our focuses + double focusColonizeHighTierWeight = 1.0; + + //How many potential designs are evaluated before choosing the best one + uint designEvaluateCount = 10; + //How long a fleet has to be fully idle before it returns to its stationed system + double fleetIdleReturnStationedTime = 60.0; + //How long we try to have a fleet be capable of firing before running out of supplies + double fleetAimSupplyDuration = 2.0 * 60.0; + + //How long a potential construction can take at most before we consider it unreasonable + double constructionMaxTime = 10.0 * 60.0; + //How long a factory has to have been idle for us to consider constructing labor storage + double laborStoreIdleTimer = 60.0; + //Maximum amount of time worth of labor we want to store in our warehouses + double laborStoreMaxFillTime = 60.0 * 10.0; + //Whether to use labor to build asteroids in the background + bool backgroundBuildAsteroids = true; + //Whether to choose the best resource on an asteroid, instead of doing it randomly + bool chooseAsteroidResource = true; + //Whether to distribute labor to shipyards when planets are idle + bool distributeLaborExports = true; + //Whether to build a shipyard to consolidate multiple planets of labor where possible + bool consolidateLaborExports = true; + //Estimate amount of labor spent per point of support ship size + double estSizeSupportLabor = 0.25; + + //Maximum combat fleets we can have in service at once (counts starting fleet(s)) + uint maxActiveFleets = UINT_MAX; + //How much flagship size we try to make per available money + double shipSizePerMoney = 1.0 / 3.5; + //How much flagship size we try to make per available labor + double shipSizePerLabor = 1.0 / 0.33; + //How much maintenance we expect per ship size + double maintenancePerShipSize = 2.0; + //Minimum percentage increase in size before we decide to retrofit a flagship to be bigger + double shipRetrofitThreshold = 0.5; + //Whether to retrofit our free starting fleet if appropriate + bool retrofitFreeFleets = false; + //Minimum percentage of average current flagship size new fleets should be + double flagshipBuildMinAvgSize = 1.00; + //Minimum game time before we consider constructing new flagships + double flagshipBuildMinGameTime = 4.0 * 60.0; + //Whether to build factories when we need labor + bool buildFactoryForLabor = true; + //Whether to build warehouses when we're not using labor + bool buildLaborStorage = true; + //Whether factories can queue labor resource imports when needed + bool allowRequestLaborImports = true; + //Whether fleets with ghosted supports attempt to rebuild the ghosts or just clear them + bool fleetsRebuildGhosts = true; + //When trying to order supports on a fleet, wait for the planet to construct its supports so we can claim them + bool supportOrderWaitOnFactory = true; + + //How much stronger we need to be than a remnant fleet to clear it + double remnantOverkillFactor = 1.5; + //Whether to allow idle fleets to be sent to clear remnants + // Modified by Relations + bool remnantAllowArbitraryClear = true; + + //Whether we should aggressively try to take out enemies + bool aggressive = false; + //Whether to become aggressive after we get boxed in and can no longer expand anywhere + bool aggressiveWhenBoxedIn = false; + //Whether we should never declare war ourselves + bool passive = false; + //Whether to hate human players the most + bool biased = false; + //How much stronger we need to be than someone to declare war out of hatred + double hatredWarOverkill = 0.5; + //How much stronger we need to be than someone to try to take them out in an aggressive war + double aggressiveWarOverkill = 1.5; + //How much stronger we want to be before we attack a system + double attackStrengthOverkill = 1.5; + //How many battles we can be performing at once + uint maxBattles = UINT_MAX; + //How much we try to overkill while fighting + double battleStrengthOverkill = 1.5; + //How many fleets we don't commit to attacks when we're already currently fighting + uint battleReserveFleets = 1; + //How much extra supply we try to have before starting a capture, to make sure we can actually do it + double captureSupplyEstimate = 1.5; + //Maximum hop distance we use as staging areas for our attacks + int stagingMaxHops = 5; + //If our fleet fill is less than this, immediately move back to factory from staging + double stagingToFactoryFill = 0.6; + + //How much ftl is reserved for critical applications + double ftlReservePctCritical = 0.25; + //How much ftl is reserved to not be used for background applications + double ftlReservePctNormal = 0.25; + + //How many artifacts we consider where to use per focus turn + uint artifactFocusConsiderCount = 2; + + //How long after trying to build a generically requested building we give up + double genericBuildExpire = 3.0 * 60.0; + + //How much the hate in a relationship decays to every minute + double hateDecayRate = 0.9; + //How much weaker we need to be to even consider surrender + double surrenderMinStrength = 0.5; + //How many of our total war points have to be taken by an empire for us to surrender + double acceptSurrenderRatio = 0.75; + double offerSurrenderRatio = 0.5; + + void setDifficulty(int diff, uint flags) { + //This changes the behavior values based on difficulty and flags + if(flags & AIF_Aggressive != 0) + aggressive = true; + if(flags & AIF_Passive != 0) + passive = true; + if(flags & AIF_Biased != 0) + biased = true; + + //Low difficulties can't colonize as many things at once + if(diff <= 0) { + maxConcurrentColonizations = 1; + guaranteeColonizations = 1; + weightOutwardExpand = 0.5; + } + else if(diff <= 1) { + maxConcurrentColonizations = 2; + weightOutwardExpand = 1.0; + } + + //Hard AI becomes aggressive when it gets boxed in + aggressiveWhenBoxedIn = diff >= 2; + + //Easy difficulty can't attack and defend at the same time + if(diff <= 0) + maxBattles = 1; + + //Low difficulties aren't as good at managing labor + if(diff <= 0) { + distributeLaborExports = false; + consolidateLaborExports = false; + buildLaborStorage = false; + } + else if(diff <= 1) { + consolidateLaborExports = false; + } + + //Low difficulties aren't as good at managing fleets + if(diff <= 0) { + maxActiveFleets = 2; + retrofitFreeFleets = true; + } + + //Low difficulties aren't as good at scouting + if(diff <= 1) + scoutAllTimer = 0.0; + + //Low difficulties are worse at designing + if(diff <= 0) + designEvaluateCount = 3; + else if(diff <= 1) + designEvaluateCount = 8; + else + designEvaluateCount = 12; + + //Easy is a bit slow + if(diff <= 0) + focusPerTick = 1; + else if(diff >= 2) + focusPerTick = 3; + } +}; + +final class AIDefs { + const BuildingType@ Factory; + const BuildingType@ LaborStorage; + const ConstructionType@ MoonBase; + const OrbitalModule@ Shipyard; + const OrbitalModule@ TradeOutpost; + const OrbitalModule@ TradeStation; +}; + +final class AI : AIController, Savable { + Empire@ empire; + AIBehavior behavior; + AIDefs defs; + + int cycleId = -1; + uint componentCycle = 0; + uint addedComponents = 0; + + uint majorMask = 0; + uint difficulty = 0; + uint flags = 0; + bool isLoading = false; + + array components; + array profileData; + IAIComponent@ events; + IAIComponent@ fleets; + IAIComponent@ budget; + IAIComponent@ colonization; + IAIComponent@ resources; + IAIComponent@ planets; + IAIComponent@ systems; + IAIComponent@ scouting; + IAIComponent@ development; + IAIComponent@ designs; + IAIComponent@ construction; + IAIComponent@ military; + IAIComponent@ movement; + IAIComponent@ creeping; + IAIComponent@ relations; + IAIComponent@ intelligence; + IAIComponent@ war; + IAIComponent@ research; + IAIComponent@ energy; + IAIComponent@ diplomacy; + IAIComponent@ consider; + IAIComponent@ orbitals; + IAIComponent@ infrastructure; + + IAIComponent@ ftl; + IAIComponent@ race; + + IAIComponent@ invasion; + + void createComponents() { + //NOTE: This is also save/load order, so + //make sure to add loading logic when changing this list + @events = add(createEvents()); + @budget = add(createBudget()); + @planets = add(createPlanets()); + @resources = add(createResources()); + @systems = add(createSystems()); + @colonization = add(createColonization()); + @fleets = add(createFleets()); + @scouting = add(createScouting()); + @development = add(createDevelopment()); + @designs = add(createDesigns()); + @construction = add(createConstruction()); + @military = add(createMilitary()); + @movement = add(createMovement()); + @creeping = add(createCreeping()); + @relations = add(createRelations()); + @intelligence = add(createIntelligence()); + @war = add(createWar()); + @research = add(createResearch()); + @energy = add(createEnergy()); + @diplomacy = add(createDiplomacy()); + @consider = add(createConsider()); + @orbitals = add(createOrbitals()); + @infrastructure = add(createInfrastructure()); + + //Make FTL component + if(empire.hasTrait(getTraitID("Hyperdrive"))) + @ftl = add(createHyperdrive()); + else if(empire.hasTrait(getTraitID("Gate"))) + @ftl = add(createGate()); + else if(empire.hasTrait(getTraitID("Fling"))) + @ftl = add(createFling()); + else if(empire.hasTrait(getTraitID("Slipstream"))) + @ftl = add(createSlipstream()); + else if(empire.hasTrait(getTraitID("Jumpdrive"))) + @ftl = add(createJumpdrive()); + /* Not implemented yet. + else if(empire.hasTrait(getTraitID("Flux"))) + @ftl = add(createFlux()); + */ + + //Make racial component + if(empire.hasTrait(getTraitID("Verdant"))) + @race = add(createVerdant()); + else if(empire.hasTrait(getTraitID("Mechanoid"))) + @race = add(createMechanoid()); + else if(empire.hasTrait(getTraitID("StarChildren"))) + @race = add(createStarChildren()); + else if(empire.hasTrait(getTraitID("Extragalactic"))) + @race = add(createExtragalactic()); + else if(empire.hasTrait(getTraitID("Linked"))) + @race = add(createLinked()); + else if(empire.hasTrait(getTraitID("Devout"))) + @race = add(createDevout()); + else if(empire.hasTrait(getTraitID("Ancient"))) + @race = add(createAncient()); + /* Not implemented yet. + else if(empire.hasTrait(getTraitID("Technicists"))) + @race = add(createResearchers()); + else if(empire.hasTrait(getTraitID("Progenitors"))) + @race = add(createProgenitors()); + else if(empire.hasTrait(getTraitID("Berserkers"))) + @race = add(createBerserkers()); + else if(empire.hasTrait(getTraitID("Pacifists"))) + @race = add(createPacifists()); + */ + + //Misc components + if(hasInvasionMap() || addedComponents & AC_Invasion != 0) { + @invasion = add(createInvasion()); + addedComponents |= AC_Invasion; + } + + //if(empire is playerEmpire) { + //log(race); + // log(colonization); + // log(resources); + // log(construction); + //} + //log(intelligence); + //logAll(); + logCritical(); + + profileData.length = components.length; + for(uint i = 0, cnt = components.length; i < cnt; ++i) + components[i].create(); + } + + void createGeneral() { + } + + void init(Empire& emp, EmpireSettings& settings) { + @this.empire = emp; + flags = settings.aiFlags; + difficulty = settings.difficulty; + behavior.setDifficulty(difficulty, flags); + + createComponents(); + } + + int getDifficultyLevel() { + return difficulty; + } + + void load(SaveFile& file) { + file >> empire; + file >> cycleId; + file >> majorMask; + file >> difficulty; + file >> flags; + if(file >= SV_0153) + file >> addedComponents; + behavior.setDifficulty(difficulty, flags); + createComponents(); + createGeneral(); + + uint loadCnt = 0; + file >> loadCnt; + loadCnt = loadCnt; + for(uint i = 0; i < loadCnt; ++i) { + double prevFocus = 0; + file >> prevFocus; + components[i].setPrevFocus(prevFocus); + file >> components[i]; + + uint check = 0; + file >> check; + if(check != GUARD) + error("ERROR: AI Load error detected in component "+addrstr(components[i])+" of "+empire.name); + } + for(uint i = 0, cnt = components.length; i < cnt; ++i) + components[i].postLoad(this); + isLoading = true; + } + + void save(SaveFile& file) { + file << empire; + file << cycleId; + file << majorMask; + file << difficulty; + file << flags; + file << addedComponents; + uint saveCnt = components.length; + file << saveCnt; + for(uint i = 0; i < saveCnt; ++i) { + file << components[i].getPrevFocus(); + file << components[i]; + file << GUARD; + } + for(uint i = 0, cnt = components.length; i < cnt; ++i) + components[i].postSave(this); + } + + void log(IAIComponent@ comp) { + if(comp is null) + return; + comp.setLog(); + comp.setLogCritical(); + } + + void logCritical() { + for(uint i = 0, cnt = components.length; i < cnt; ++i) + components[i].setLogCritical(); + } + + void logAll() { + for(uint i = 0, cnt = components.length; i < cnt; ++i) { + components[i].setLog(); + components[i].setLogCritical(); + } + } + + IAIComponent@ add(IAIComponent& component) { + component.set(this); + components.insertLast(component); + return component; + } + + void init(Empire& emp) { + majorMask = 0; + for(uint i = 0, cnt = getEmpireCount(); i < cnt; ++i) { + Empire@ emp = getEmpire(i); + if(emp.major) + majorMask |= emp.mask; + } + + createGeneral(); + } + + bool hasStarted = false; + void tick(Empire& emp, double time) { + if(isLoading) { + for(uint i = 0, cnt = components.length; i < cnt; ++i) + components[i].loadFinalize(this); + isLoading = false; + hasStarted = true; + } + else if(!hasStarted) { + for(uint i = 0, cnt = components.length; i < cnt; ++i) + components[i].start(); + hasStarted = true; + } + else if(emp.Victory == -1) { + //Don't do anything when actually defeated + return; + } + + //Manage gametime-specific behaviors + behavior.colonizeGenericExpand = gameTime >= 6.0 * 60.0; + + //Find cycled turns + int curCycle = emp.BudgetCycleId; + if(curCycle != cycleId) { + for(uint i = 0, cnt = components.length; i < cnt; ++i) + components[i].turn(); + cycleId = curCycle; + } + + //Generic ticks + double startTime = getExactTime(); + for(uint i = 0, cnt = components.length; i < cnt; ++i) { + auto@ comp = components[i]; + comp.tick(time); + + double endTime = getExactTime(); + //double ms = 1000.0 * (endTime - startTime); + startTime = endTime; + + //auto@ dat = profileData[i]; + //dat.tickPeak = max(dat.tickPeak, ms); + //dat.tickAvg += ms; + //dat.tickCount += 1.0; + } + + //Do focuseds tick on components + uint focusCount = behavior.focusPerTick; + if(ai_full_speed.value == 1.0) + focusCount = max(uint(round((time / 0.25) * behavior.focusPerTick)), behavior.focusPerTick); + double allocStart = startTime; + + for(uint n = 0; n < focusCount; ++n) { + componentCycle = (componentCycle+1) % components.length; + auto@ focusComp = components[componentCycle]; + focusComp.focusTick(gameTime - focusComp.getPrevFocus()); + focusComp.setPrevFocus(gameTime); + + double endTime = getExactTime(); + //double ms = 1000.0 * (endTime - startTime); + startTime = endTime; + if(endTime - allocStart > 4000.0) + break; + + //auto@ dat = profileData[componentCycle]; + //dat.focusPeak = max(dat.focusPeak, ms); + //dat.focusAvg += ms; + //dat.focusCount += 1.0; + } + } + + void dumpProfile() { + for(uint i = 0, cnt = components.length; i < cnt; ++i) { + auto@ c = profileData[i]; + print(pad(addrstr(components[i]), 40)+" tick peak "+toString(c.tickPeak,2)+" tick avg "+toString(c.tickAvg/c.tickCount, 2) + +" focus peak "+toString(c.focusPeak,2)+" focus avg "+toString(c.focusAvg/c.focusCount, 2)); + } + } + + void resetProfile() { + for(uint i = 0, cnt = profileData.length; i < cnt; ++i) { + auto@ c = profileData[i]; + c.tickPeak = 0.0; + c.tickAvg = 0.0; + c.tickCount = 0.0; + c.focusPeak = 0.0; + c.focusAvg = 0.0; + c.focusCount = 0.0; + } + } + + uint get_mask() { + return empire.mask; + } + + uint get_teamMask() { + //TODO + return empire.mask; + } + + uint get_visionMask() { + return majorMask & empire.visionMask; + } + + uint get_allyMask() { + return empire.mutualDefenseMask | empire.ForcedPeaceMask.value; + } + + uint get_enemyMask() { + return empire.hostileMask & majorMask; + } + + uint get_neutralMask() { + return majorMask & ~allyMask & ~mask & ~enemyMask; + } + + uint get_otherMask() { + return majorMask & ~mask; + } + + string pad(const string& input, uint width) { + string str = input; + while(str.length < width) + str += " "; + return str; + } + + void print(const string& info, Object@ related = null, double value = INFINITY, bool flag = false, Empire@ emp = null) { + string str = info; + if(related !is null) + str = pad(related.name, 16)+" | "+str; + str = pad("["+empire.index+": "+empire.name+" AI] ", 20)+str; + str = formatGameTime(gameTime) + " " + str; + if(value != INFINITY) + str += " | Value = "+standardize(value, true); + if(flag) + str += " | FLAGGED On"; + if(emp !is null) + str += " | Target = "+emp.name; + ::print(str); + } + + void debugAI() {} + void commandAI(string cmd) { + if (cmd == "forbid all") { + behavior.forbidDiplomacy = true; + behavior.forbidColonization = true; + behavior.forbidCreeping = true; + behavior.forbidResearch = true; + behavior.forbidDefense = true; + behavior.forbidAttack = true; + behavior.forbidConstruction = true; + behavior.forbidScouting = true; + behavior.forbidAnomalyChoice = true; + behavior.forbidArtifact = true; + behavior.forbidScuttle = true; + } else if (cmd == "allow all") { + behavior.forbidDiplomacy = false; + behavior.forbidColonization = false; + behavior.forbidCreeping = false; + behavior.forbidResearch = false; + behavior.forbidDefense = false; + behavior.forbidAttack = false; + behavior.forbidConstruction = false; + behavior.forbidScouting = false; + behavior.forbidAnomalyChoice = false; + behavior.forbidArtifact = false; + behavior.forbidScuttle = false; + } else if (cmd == "forbid Diplomacy") { behavior.forbidDiplomacy = true; + } else if (cmd == "allow Diplomacy") { behavior.forbidDiplomacy = false; + } else if (cmd == "forbid Colonization") { behavior.forbidColonization = true; + } else if (cmd == "allow Colonization") { behavior.forbidColonization = false; + } else if (cmd == "forbid Creeping") { behavior.forbidCreeping = true; + } else if (cmd == "allow Creeping") { behavior.forbidCreeping = false; + } else if (cmd == "forbid Research") { behavior.forbidResearch = true; + } else if (cmd == "allow Research") { behavior.forbidResearch = false; + } else if (cmd == "forbid Defense") { behavior.forbidDefense = true; + } else if (cmd == "allow Defense") { behavior.forbidDefense = false; + } else if (cmd == "forbid Attack") { behavior.forbidAttack = true; + } else if (cmd == "allow Attack") { behavior.forbidAttack = false; + } else if (cmd == "forbid Construction") { behavior.forbidConstruction = true; + } else if (cmd == "allow Construction") { behavior.forbidConstruction = false; + } else if (cmd == "forbid Scouting") { behavior.forbidScouting = true; + } else if (cmd == "allow Scouting") { behavior.forbidScouting = false; + } else if (cmd == "forbid AnomalyChoice") { behavior.forbidAnomalyChoice = true; + } else if (cmd == "allow AnomalyChoice") { behavior.forbidAnomalyChoice = false; + } else if (cmd == "forbid Artifact") { behavior.forbidArtifact = true; + } else if (cmd == "allow Artifact") { behavior.forbidArtifact = false; + } else if (cmd == "forbid Scuttle") { behavior.forbidScuttle = true; + } else if (cmd == "allow Scuttle") { behavior.forbidScuttle = false; + } else { + print("WeaselAI: got unhandled AI command: " + cmd); + } + } + void aiPing(Empire@ fromEmpire, vec3d position, uint type) {} + void pause(Empire& emp) {} + void resume(Empire& emp) {} + vec3d get_aiFocus() { return vec3d(); } + string getOpinionOf(Empire& emp, Empire@ other) { return ""; } + int getStandingTo(Empire& emp, Empire@ other) { return 0; } +}; + +AIController@ createWeaselAI() { + return AI(); +} ADDED scripts/server/empire_ai/weasel/debug.as Index: scripts/server/empire_ai/weasel/debug.as ================================================================== --- scripts/server/empire_ai/weasel/debug.as +++ scripts/server/empire_ai/weasel/debug.as @@ -0,0 +1,167 @@ +import empire_ai.weasel.WeaselAI; +import empire_ai.weasel.Colonization; +import empire_ai.weasel.Construction; +import empire_ai.weasel.Budget; +import empire_ai.weasel.Designs; +import empire_ai.weasel.Development; +import empire_ai.weasel.Fleets; +import empire_ai.weasel.Military; +import empire_ai.weasel.Planets; +import empire_ai.weasel.Resources; +import empire_ai.weasel.Scouting; +import empire_ai.weasel.Systems; +import empire_ai.weasel.Creeping; +import empire_ai.weasel.Movement; +import empire_ai.weasel.Relations; +import empire_ai.weasel.Intelligence; +import empire_ai.weasel.War; +import empire_ai.weasel.Research; +import empire_ai.weasel.Energy; +import empire_ai.weasel.Diplomacy; +import empire_ai.weasel.ftl.Gate; +import empire_ai.weasel.ftl.Hyperdrive; +import empire_ai.weasel.ftl.Fling; +import empire_ai.weasel.ftl.Slipstream; +import empire_ai.weasel.ftl.Jumpdrive; +import empire_ai.weasel.race.Verdant; +import empire_ai.weasel.race.Mechanoid; +import empire_ai.weasel.race.StarChildren; +import empire_ai.weasel.race.Extragalactic; +import empire_ai.weasel.race.Linked; +import empire_ai.weasel.race.Devout; +import empire_ai.weasel.race.Ancient; +import empire_ai.weasel.misc.Invasion; +import empire_ai.EmpireAI; + +AI@ ai(uint index) { + Empire@ emp = getEmpire(index); + return cast(cast(emp.EmpireAI).ctrl); +} + +Colonization@ colonization(uint index) { + return cast(ai(index).colonization); +} + +Construction@ construction(uint index) { + return cast(ai(index).construction); +} + +Budget@ budget(uint index) { + return cast(ai(index).budget); +} + +Designs@ designs(uint index) { + return cast(ai(index).designs); +} + +Development@ development(uint index) { + return cast(ai(index).development); +} + +Fleets@ fleets(uint index) { + return cast(ai(index).fleets); +} + +Military@ military(uint index) { + return cast(ai(index).military); +} + +Planets@ planets(uint index) { + return cast(ai(index).planets); +} + +Resources@ resources(uint index) { + return cast(ai(index).resources); +} + +Scouting@ scouting(uint index) { + return cast(ai(index).scouting); +} + +Systems@ systems(uint index) { + return cast(ai(index).systems); +} + +Movement@ movement(uint index) { + return cast(ai(index).movement); +} + +Creeping@ creeping(uint index) { + return cast(ai(index).creeping); +} + +Relations@ relations(uint index) { + return cast(ai(index).relations); +} + +Intelligence@ intelligence(uint index) { + return cast(ai(index).intelligence); +} + +War@ war(uint index) { + return cast(ai(index).war); +} + +Research@ research(uint index) { + return cast(ai(index).research); +} + +Energy@ energy(uint index) { + return cast(ai(index).energy); +} + +Diplomacy@ diplomacy(uint index) { + return cast(ai(index).diplomacy); +} + +Gate@ gate(uint index) { + return cast(ai(index).ftl); +} + +Hyperdrive@ hyperdrive(uint index) { + return cast(ai(index).ftl); +} + +Fling@ fling(uint index) { + return cast(ai(index).ftl); +} + +Slipstream@ slipstream(uint index) { + return cast(ai(index).ftl); +} + +Jumpdrive@ jumpdrive(uint index) { + return cast(ai(index).ftl); +} + +Mechanoid@ mechanoid(uint index) { + return cast(ai(index).race); +} + +Verdant@ verdant(uint index) { + return cast(ai(index).race); +} + +StarChildren@ starchildren(uint index) { + return cast(ai(index).race); +} + +Extragalactic@ extragalactic(uint index) { + return cast(ai(index).race); +} + +Linked@ linked(uint index) { + return cast(ai(index).race); +} + +Devout@ devout(uint index) { + return cast(ai(index).race); +} + +Ancient@ ancient(uint index) { + return cast(ai(index).race); +} + +Invasion@ invasion(uint index) { + return cast(ai(index).invasion); +} ADDED scripts/server/empire_ai/weasel/ftl/Fling.as Index: scripts/server/empire_ai/weasel/ftl/Fling.as ================================================================== --- scripts/server/empire_ai/weasel/ftl/Fling.as +++ scripts/server/empire_ai/weasel/ftl/Fling.as @@ -0,0 +1,402 @@ +import empire_ai.weasel.WeaselAI; +import empire_ai.weasel.Movement; +import empire_ai.weasel.Military; +import empire_ai.weasel.Construction; +import empire_ai.weasel.Designs; +import empire_ai.weasel.Development; +import empire_ai.weasel.Systems; +import empire_ai.weasel.Budget; +import empire_ai.weasel.Fleets; + +import ftl; +from orbitals import getOrbitalModuleID; + +const double FLING_MIN_DISTANCE_STAGE = 10000; +const double FLING_MIN_DISTANCE_DEVELOP = 20000; +const double FLING_MIN_TIMER = 3.0 * 60.0; + +int flingModule = -1; + +void init() { + flingModule = getOrbitalModuleID("FlingCore"); +} + +class FlingRegion : Savable { + Region@ region; + Object@ obj; + bool installed = false; + vec3d destination; + + void save(SaveFile& file) { + file << region; + file << obj; + file << installed; + file << destination; + } + + void load(SaveFile& file) { + file >> region; + file >> obj; + file >> installed; + file >> destination; + } +}; + +class Fling : FTL { + Military@ military; + Designs@ designs; + Construction@ construction; + Development@ development; + Systems@ systems; + Budget@ budget; + Fleets@ fleets; + + array tracked; + array unused; + + BuildOrbital@ buildFling; + double nextBuildTry = 15.0 * 60.0; + bool wantToBuild = false; + + void create() override { + @military = cast(ai.military); + @designs = cast(ai.designs); + @construction = cast(ai.construction); + @development = cast(ai.development); + @systems = cast(ai.systems); + @budget = cast(ai.budget); + @fleets = cast(ai.fleets); + } + + void save(SaveFile& file) override { + construction.saveConstruction(file, buildFling); + file << nextBuildTry; + file << wantToBuild; + + uint cnt = tracked.length; + file << cnt; + for(uint i = 0; i < cnt; ++i) + file << tracked[i]; + + cnt = unused.length; + file << cnt; + for(uint i = 0; i < cnt; ++i) + file << unused[i]; + } + + void load(SaveFile& file) override { + @buildFling = cast(construction.loadConstruction(file)); + file >> nextBuildTry; + file >> wantToBuild; + + uint cnt = 0; + file >> cnt; + for(uint i = 0; i < cnt; ++i) { + FlingRegion fr; + file >> fr; + tracked.insertLast(fr); + } + + file >> cnt; + for(uint i = 0; i < cnt; ++i) { + Object@ obj; + file >> obj; + if(obj !is null) + unused.insertLast(obj); + } + } + + uint order(MoveOrder& ord) override { + if(!canFling(ord.obj)) + return F_Pass; + + //Find the position to fling to + vec3d toPosition; + if(!targetPosition(ord, toPosition)) + return F_Pass; + + //Don't fling if we're saving our FTL for a new beacon + double avail = usableFTL(ai, ord); + if((buildFling !is null && !buildFling.started) || wantToBuild) + avail = min(avail, ai.empire.FTLStored - 250.0); + + //Make sure we have the ftl to fling + if(flingCost(ord.obj, toPosition) > avail) + return F_Pass; + + //Make sure we're in range of a beacon + Object@ beacon = getClosest(ord.obj.position); + if(beacon is null || beacon.position.distanceTo(ord.obj.position) > FLING_BEACON_RANGE) + return F_Pass; + + ord.obj.addFlingOrder(beacon, toPosition); + return F_Continue; + } + + FlingRegion@ get(Region@ reg) { + for(uint i = 0, cnt = tracked.length; i < cnt; ++i) { + if(tracked[i].region is reg) + return tracked[i]; + } + return null; + } + + void remove(FlingRegion@ gt) { + if(gt.obj !is null && gt.obj.valid && gt.obj.owner is ai.empire) + unused.insertLast(gt.obj); + tracked.remove(gt); + } + + Object@ getClosest(const vec3d& position) { + Object@ closest; + double minDist = INFINITY; + for(uint i = 0, cnt = tracked.length; i < cnt; ++i) { + Object@ obj = tracked[i].obj; + if(obj is null) + continue; + double d = obj.position.distanceTo(position); + if(d < minDist) { + minDist = d; + @closest = obj; + } + } + for(uint i = 0, cnt = unused.length; i < cnt; ++i) { + Object@ obj = unused[i]; + if(obj is null) + continue; + double d = obj.position.distanceTo(position); + if(d < minDist) { + minDist = d; + @closest = obj; + } + } + return closest; + } + + FlingRegion@ getClosestRegion(const vec3d& position) { + FlingRegion@ closest; + double minDist = INFINITY; + for(uint i = 0, cnt = tracked.length; i < cnt; ++i) { + double d = tracked[i].region.position.distanceTo(position); + if(d < minDist) { + minDist = d; + @closest = tracked[i]; + } + } + return closest; + } + + void assignTo(FlingRegion@ track, Object@ closest) { + unused.remove(closest); + @track.obj = closest; + } + + bool trackingBeacon(Object@ obj) { + for(uint i = 0, cnt = unused.length; i < cnt; ++i) { + if(unused[i] is obj) + return true; + } + for(uint i = 0, cnt = tracked.length; i < cnt; ++i) { + if(tracked[i].obj is obj) + return true; + } + return false; + } + + bool shouldHaveBeacon(Region@ reg, bool always = false) { + if(military.getBase(reg) !is null) + return true; + if(development.isDevelopingIn(reg)) + return true; + return false; + } + + void focusTick(double time) override { + //Manage unused beacons list + for(uint i = 0, cnt = unused.length; i < cnt; ++i) { + Object@ obj = unused[i]; + if(obj is null || !obj.valid || obj.owner !is ai.empire) { + unused.removeAt(i); + --i; --cnt; + } + } + + if(ai.behavior.forbidConstruction) return; + + //Detect new beacons + auto@ data = ai.empire.getFlingBeacons(); + Object@ obj; + while(receive(data, obj)) { + if(obj is null) + continue; + if(!trackingBeacon(obj)) + unused.insertLast(obj); + } + + //Update existing beacons for staging bases + for(uint i = 0, cnt = tracked.length; i < cnt; ++i) { + auto@ reg = tracked[i]; + bool checkAlways = false; + if(reg.obj !is null) { + if(!reg.obj.valid || reg.obj.owner !is ai.empire || reg.obj.region !is reg.region) { + @reg.obj = null; + checkAlways = true; + } + } + if(!shouldHaveBeacon(reg.region, checkAlways)) { + remove(tracked[i]); + --i; --cnt; + } + } + + //Detect new staging bases to build beacons at + for(uint i = 0, cnt = military.stagingBases.length; i < cnt; ++i) { + auto@ base = military.stagingBases[i]; + if(base.occupiedTime < FLING_MIN_TIMER) + continue; + + if(get(base.region) is null) { + FlingRegion@ closest = getClosestRegion(base.region.position); + if(closest !is null && closest.region.position.distanceTo(base.region.position) < FLING_MIN_DISTANCE_STAGE) + continue; + + FlingRegion gt; + @gt.region = base.region; + tracked.insertLast(gt); + break; + } + } + + //Detect new important planets to build beacons at + for(uint i = 0, cnt = development.focuses.length; i < cnt; ++i) { + auto@ focus = development.focuses[i]; + Region@ reg = focus.obj.region; + if(reg is null) + continue; + + if(get(reg) is null) { + FlingRegion@ closest = getClosestRegion(reg.position); + if(closest !is null && closest.region.position.distanceTo(reg.position) < FLING_MIN_DISTANCE_DEVELOP) + continue; + + FlingRegion gt; + @gt.region = reg; + tracked.insertLast(gt); + break; + } + } + + //Destroy beacons if we're having ftl trouble + if(ai.empire.FTLShortage && !ai.behavior.forbidScuttle) { + Orbital@ leastImportant; + double leastWeight = INFINITY; + + for(uint i = 0, cnt = unused.length; i < cnt; ++i) { + Orbital@ obj = cast(unused[i]); + if(obj is null || !obj.valid) + continue; + + @leastImportant = obj; + leastWeight = 0.0; + break; + } + + if(leastImportant !is null) { + if(log) + ai.print("Scuttle unused beacon for ftl", leastImportant.region); + leastImportant.scuttle(); + } + else { + for(uint i = 0, cnt = tracked.length; i < cnt; ++i) { + Orbital@ obj = cast(tracked[i].obj); + if(obj is null || !obj.valid) + continue; + + double weight = 1.0; + auto@ base = military.getBase(tracked[i].region); + if(base is null) { + weight *= 5.0; + } + else if(base.idleTime >= 1) { + weight *= 1.0 + (base.idleTime / 60.0); + } + else { + weight /= 2.0; + } + + if(weight < leastWeight) { + @leastImportant = obj; + leastWeight = weight; + } + } + + if(leastImportant !is null) { + if(log) + ai.print("Scuttle unimportant beacon for ftl", leastImportant.region); + leastImportant.scuttle(); + } + } + } + + //See if we should build a new gate + if(buildFling !is null) { + if(buildFling.completed) { + @buildFling = null; + nextBuildTry = gameTime + 60.0; + } + } + wantToBuild = false; + for(uint i = 0, cnt = tracked.length; i < cnt; ++i) { + auto@ gt = tracked[i]; + if(gt.obj is null && gt.region.ContestedMask & ai.mask == 0 && gt.region.BlockFTLMask & ai.mask == 0) { + Object@ found; + for(uint n = 0, ncnt = unused.length; n < ncnt; ++n) { + Object@ obj = unused[n]; + if(obj.region is gt.region) { + @found = obj; + break; + } + } + + if(found !is null) { + if(log) + ai.print("Assign beacon to => "+gt.region.name, found.region); + assignTo(gt, found); + } else if(buildFling is null && gameTime > nextBuildTry && !ai.empire.isFTLShortage(0.15)) { + if(ai.empire.FTLStored >= 250) { + if(log) + ai.print("Build beacon for this system", gt.region); + + @buildFling = construction.buildOrbital(getOrbitalModule(flingModule), military.getStationPosition(gt.region)); + } + else { + wantToBuild = true; + } + } + } + } + + //Scuttle anything unused if we don't need beacons in those regions + for(uint i = 0, cnt = unused.length; i < cnt; ++i) { + if(get(unused[i].region) is null && unused[i].isOrbital) { + cast(unused[i]).scuttle(); + unused.removeAt(i); + --i; --cnt; + } + } + + //Try to get enough ftl storage that we can fling our largest fleet and have some remaining + double highestCost = 0.0; + for(uint i = 0, cnt = fleets.fleets.length; i < cnt; ++i) { + auto@ flAI = fleets.fleets[i]; + if(flAI.fleetClass != FC_Combat) + continue; + highestCost = max(highestCost, double(flingCost(flAI.obj, vec3d()))); + } + development.aimFTLStorage = highestCost / (1.0 - ai.behavior.ftlReservePctCritical - ai.behavior.ftlReservePctNormal); + } +}; + +AIComponent@ createFling() { + return Fling(); +} ADDED scripts/server/empire_ai/weasel/ftl/Gate.as Index: scripts/server/empire_ai/weasel/ftl/Gate.as ================================================================== --- scripts/server/empire_ai/weasel/ftl/Gate.as +++ scripts/server/empire_ai/weasel/ftl/Gate.as @@ -0,0 +1,446 @@ +import empire_ai.weasel.WeaselAI; +import empire_ai.weasel.Movement; +import empire_ai.weasel.Military; +import empire_ai.weasel.Construction; +import empire_ai.weasel.Designs; +import empire_ai.weasel.Development; +import empire_ai.weasel.Systems; +import empire_ai.weasel.Budget; + +from statuses import getStatusID; +from abilities import getAbilityID; + +const double GATE_MIN_DISTANCE_STAGE = 10000; +const double GATE_MIN_DISTANCE_DEVELOP = 20000; +const double GATE_MIN_DISTANCE_BORDER = 30000; +const double GATE_MIN_TIMER = 3.0 * 60.0; +const int GATE_BUILD_MOVE_HOPS = 5; + +int packAbility = -1; +int unpackAbility = -1; + +int packedStatus = -1; +int unpackedStatus = -1; + +void init() { + packAbility = getAbilityID("GatePack"); + unpackAbility = getAbilityID("GateUnpack"); + + packedStatus = getStatusID("GatePacked"); + unpackedStatus = getStatusID("GateUnpacked"); +} + +class GateRegion : Savable { + Region@ region; + Object@ gate; + bool installed = false; + vec3d destination; + + void save(SaveFile& file) { + file << region; + file << gate; + file << installed; + file << destination; + } + + void load(SaveFile& file) { + file >> region; + file >> gate; + file >> installed; + file >> destination; + } +}; + +class Gate : FTL { + Military@ military; + Designs@ designs; + Construction@ construction; + Development@ development; + Systems@ systems; + Budget@ budget; + + DesignTarget@ gateDesign; + + array tracked; + array unassigned; + + BuildStation@ buildGate; + double nextBuildTry = 15.0 * 60.0; + + void create() override { + @military = cast(ai.military); + @designs = cast(ai.designs); + @construction = cast(ai.construction); + @development = cast(ai.development); + @systems = cast(ai.systems); + @budget = cast(ai.budget); + } + + void save(SaveFile& file) override { + designs.saveDesign(file, gateDesign); + construction.saveConstruction(file, buildGate); + file << nextBuildTry; + + uint cnt = tracked.length; + file << cnt; + for(uint i = 0; i < cnt; ++i) + file << tracked[i]; + + cnt = unassigned.length; + file << cnt; + for(uint i = 0; i < cnt; ++i) + file << unassigned[i]; + } + + void load(SaveFile& file) override { + @gateDesign = designs.loadDesign(file); + @buildGate = cast(construction.loadConstruction(file)); + file >> nextBuildTry; + + uint cnt = 0; + file >> cnt; + for(uint i = 0; i < cnt; ++i) { + GateRegion gt; + file >> gt; + tracked.insertLast(gt); + } + + file >> cnt; + for(uint i = 0; i < cnt; ++i) { + Object@ obj; + file >> obj; + if(obj !is null) + unassigned.insertLast(obj); + } + } + + GateRegion@ get(Region@ reg) { + for(uint i = 0, cnt = tracked.length; i < cnt; ++i) { + if(tracked[i].region is reg) + return tracked[i]; + } + return null; + } + + void remove(GateRegion@ gt) { + if(gt.gate !is null && gt.gate.valid && gt.gate.owner is ai.empire) + unassigned.insertLast(gt.gate); + tracked.remove(gt); + } + + Object@ getClosestGate(const vec3d& position) { + Object@ closest; + double minDist = INFINITY; + for(uint i = 0, cnt = tracked.length; i < cnt; ++i) { + Object@ gate = tracked[i].gate; + if(gate is null) + continue; + if(!tracked[i].installed) + continue; + double d = gate.position.distanceTo(position); + if(d < minDist) { + minDist = d; + @closest = gate; + } + } + return closest; + } + + GateRegion@ getClosestGateRegion(const vec3d& position) { + GateRegion@ closest; + double minDist = INFINITY; + for(uint i = 0, cnt = tracked.length; i < cnt; ++i) { + double d = tracked[i].region.position.distanceTo(position); + if(d < minDist) { + minDist = d; + @closest = tracked[i]; + } + } + return closest; + } + + void assignTo(GateRegion@ gt, Object@ closest) { + unassigned.remove(closest); + @gt.gate = closest; + gt.installed = false; + + if(closest.region is gt.region) { + if(closest.hasStatusEffect(unpackedStatus)) { + gt.installed = true; + } + } + + if(!gt.installed) { + gt.destination = military.getStationPosition(gt.region); + closest.activateAbilityTypeFor(ai.empire, packAbility); + closest.addMoveOrder(gt.destination); + } + } + + bool trackingGate(Object@ obj) { + for(uint i = 0, cnt = unassigned.length; i < cnt; ++i) { + if(unassigned[i] is obj) + return true; + } + for(uint i = 0, cnt = tracked.length; i < cnt; ++i) { + if(tracked[i].gate is obj) + return true; + } + return false; + } + + bool shouldHaveGate(Region@ reg, bool always = false) { + if(military.getBase(reg) !is null) + return true; + if(development.isDevelopingIn(reg)) + return true; + if(!always) { + auto@ sys = systems.getAI(reg); + if(sys !is null) { + if(sys.border && sys.bordersEmpires) + return true; + } + } + return false; + } + + void turn() override { + if(gateDesign !is null && gateDesign.active !is null) { + int newSize = round(double(budget.spendable(BT_Military)) * 0.5 * ai.behavior.shipSizePerMoney / 64.0) * 64; + if(newSize < 128) + newSize = 128; + if(newSize != gateDesign.targetSize) { + @gateDesign = designs.design(DP_Gate, newSize); + gateDesign.customName = "Gate"; + } + } + } + + void focusTick(double time) override { + if(ai.behavior.forbidConstruction) return; + + //Design a gate + if(gateDesign is null) { + @gateDesign = designs.design(DP_Gate, 128); + gateDesign.customName = "Gate"; + } + + //Manage unassigned gates list + for(uint i = 0, cnt = unassigned.length; i < cnt; ++i) { + Object@ obj = unassigned[i]; + if(obj is null || !obj.valid || obj.owner !is ai.empire) { + unassigned.removeAt(i); + --i; --cnt; + } + } + + //Detect new gates + auto@ data = ai.empire.getStargates(); + Object@ obj; + while(receive(data, obj)) { + if(obj is null) + continue; + if(!trackingGate(obj)) + unassigned.insertLast(obj); + } + + //Update existing gates for staging bases + for(uint i = 0, cnt = tracked.length; i < cnt; ++i) { + auto@ gt = tracked[i]; + bool checkAlways = false; + if(gt.gate !is null) { + if(!gt.gate.valid || gt.gate.owner !is ai.empire || (gt.installed && gt.gate.region !is gt.region)) { + @gt.gate = null; + gt.installed = false; + checkAlways = true; + } + else if(!gt.installed && !gt.gate.hasOrders) { + if(gt.destination.distanceTo(gt.gate.position) < 10.0) { + gt.gate.activateAbilityTypeFor(ai.empire, unpackAbility, gt.destination); + gt.installed = true; + } + else { + gt.gate.activateAbilityTypeFor(ai.empire, packAbility); + gt.gate.addMoveOrder(gt.destination); + } + } + } + if(!shouldHaveGate(gt.region, checkAlways)) { + remove(tracked[i]); + --i; --cnt; + } + } + + //Detect new staging bases to build gates at + for(uint i = 0, cnt = military.stagingBases.length; i < cnt; ++i) { + auto@ base = military.stagingBases[i]; + if(base.occupiedTime < GATE_MIN_TIMER) + continue; + + if(get(base.region) is null) { + GateRegion@ closest = getClosestGateRegion(base.region.position); + if(closest !is null && closest.region.position.distanceTo(base.region.position) < GATE_MIN_DISTANCE_STAGE) + continue; + + GateRegion gt; + @gt.region = base.region; + tracked.insertLast(gt); + break; + } + } + + //Detect new important planets to build gates at + for(uint i = 0, cnt = development.focuses.length; i < cnt; ++i) { + auto@ focus = development.focuses[i]; + Region@ reg = focus.obj.region; + if(reg is null) + continue; + + if(get(reg) is null) { + GateRegion@ closest = getClosestGateRegion(reg.position); + if(closest !is null && closest.region.position.distanceTo(reg.position) < GATE_MIN_DISTANCE_DEVELOP) + continue; + + GateRegion gt; + @gt.region = reg; + tracked.insertLast(gt); + break; + } + } + + //Detect new border systems to build gates at + uint offset = randomi(0, systems.border.length-1); + for(uint i = 0, cnt = systems.border.length; i < cnt; ++i) { + auto@ sys = systems.border[(i+offset)%cnt]; + Region@ reg = sys.obj; + if(reg is null) + continue; + if(!sys.bordersEmpires) + continue; + + if(get(reg) is null) { + GateRegion@ closest = getClosestGateRegion(reg.position); + if(closest !is null && closest.region.position.distanceTo(reg.position) < GATE_MIN_DISTANCE_DEVELOP) + continue; + + GateRegion gt; + @gt.region = reg; + tracked.insertLast(gt); + break; + } + } + + //Destroy gates if we're having ftl trouble + if(ai.empire.FTLShortage && !ai.behavior.forbidScuttle) { + Ship@ leastImportant; + double leastWeight = INFINITY; + + for(uint i = 0, cnt = unassigned.length; i < cnt; ++i) { + Ship@ ship = cast(unassigned[i]); + if(ship is null || !ship.valid) + continue; + + double weight = ship.blueprint.design.size; + weight *= 10.0; + + if(weight < leastWeight) { + @leastImportant = ship; + leastWeight = weight; + } + } + + if(leastImportant !is null) { + if(log) + ai.print("Scuttle unassigned gate for ftl", leastImportant.region); + leastImportant.scuttle(); + } + else { + for(uint i = 0, cnt = tracked.length; i < cnt; ++i) { + Ship@ ship = cast(tracked[i].gate); + if(ship is null || !ship.valid) + continue; + + double weight = ship.blueprint.design.size; + auto@ base = military.getBase(tracked[i].region); + if(base is null) { + weight *= 5.0; + } + else if(base.idleTime >= 1) { + weight *= 1.0 + (base.idleTime / 60.0); + } + else { + weight /= 2.0; + } + + if(weight < leastWeight) { + @leastImportant = ship; + leastWeight = weight; + } + } + + if(leastImportant !is null) { + if(log) + ai.print("Scuttle unimportant gate for ftl", leastImportant.region); + leastImportant.scuttle(); + } + } + } + + //See if we should build a new gate + if(buildGate !is null) { + if(buildGate.completed) { + @buildGate = null; + nextBuildTry = gameTime + 60.0; + } + } + for(uint i = 0, cnt = tracked.length; i < cnt; ++i) { + auto@ gt = tracked[i]; + if(gt.gate is null && gt.region.ContestedMask & ai.mask == 0 && gt.region.BlockFTLMask & ai.mask == 0) { + Object@ closest; + double closestDist = INFINITY; + for(uint n = 0, ncnt = unassigned.length; n < ncnt; ++n) { + Object@ obj = unassigned[n]; + if(obj.region is gt.region) { + @closest = obj; + break; + } + if(!obj.hasMover) + continue; + if(buildGate is null && gameTime > nextBuildTry) { + double d = obj.position.distanceTo(gt.region.position); + if(d < closestDist) { + closestDist = d; + @closest = obj; + } + } + } + + if(closest !is null) { + if(log) + ai.print("Assign gate to => "+gt.region.name, closest.region); + assignTo(gt, closest); + } else if(buildGate is null && gameTime > nextBuildTry && !ai.empire.isFTLShortage(0.15)) { + if(log) + ai.print("Build gate for this system", gt.region); + + bool buildLocal = true; + auto@ factory = construction.primaryFactory; + if(factory !is null) { + Region@ factRegion = factory.obj.region; + if(factRegion !is null && systems.hopDistance(gt.region, factRegion) < GATE_BUILD_MOVE_HOPS) + buildLocal = false; + } + + if(buildLocal) + @buildGate = construction.buildLocalStation(gateDesign); + else + @buildGate = construction.buildStation(gateDesign, military.getStationPosition(gt.region)); + } + } + } + } +}; + +AIComponent@ createGate() { + return Gate(); +} ADDED scripts/server/empire_ai/weasel/ftl/Hyperdrive.as Index: scripts/server/empire_ai/weasel/ftl/Hyperdrive.as ================================================================== --- scripts/server/empire_ai/weasel/ftl/Hyperdrive.as +++ scripts/server/empire_ai/weasel/ftl/Hyperdrive.as @@ -0,0 +1,99 @@ +import empire_ai.weasel.WeaselAI; +import empire_ai.weasel.Movement; +import empire_ai.weasel.Development; +import empire_ai.weasel.Fleets; + +import ftl; + +from orders import OrderType; + +const double REJUMP_MIN_DIST = 8000.0; +const double STORAGE_AIM_DISTANCE = 40000; + +class Hyperdrive : FTL { + Development@ development; + Fleets@ fleets; + + void create() override { + @development = cast(ai.development); + @fleets = cast(ai.fleets); + } + + double hdETA(Object& obj, const vec3d& position) { + double charge = HYPERDRIVE_CHARGE_TIME; + if(obj.owner.HyperdriveNeedCharge == 0) + charge = 0.0; + double dist = position.distanceTo(obj.position); + double speed = hyperdriveMaxSpeed(obj); + return charge + dist / speed; + } + + double subETA(Object& obj, const vec3d& position) { + return newtonArrivalTime(obj.maxAcceleration, position - obj.position, vec3d()); + } + + bool shouldHD(Object& obj, const vec3d& position, uint priority) { + //This makes me sad + if(position.distanceTo(obj.position) < 3000) + return false; + double pathDist = cast(ai.movement).getPathDistance(obj.position, position, obj.maxAcceleration); + double straightDist = position.distanceTo(obj.position); + return pathDist >= straightDist * 0.6; + } + + uint order(MoveOrder& ord) override { + if(!canHyperdrive(ord.obj)) + return F_Pass; + + double avail = usableFTL(ai, ord); + if(avail > 0) { + vec3d toPosition; + if(targetPosition(ord, toPosition)) { + if(shouldHD(ord.obj, toPosition, ord.priority)) { + double cost = hyperdriveCost(ord.obj, toPosition); + if(avail >= cost) { + ord.obj.addHyperdriveOrder(toPosition); + return F_Continue; + } + } + } + } + + return F_Pass; + } + + uint tick(MoveOrder& ord, double time) { + if(ord.priority == MP_Critical && canHyperdrive(ord.obj) && ord.obj.firstOrderType != OT_Hyperdrive) { + vec3d toPosition; + if(targetPosition(ord, toPosition)) { + double dist = ord.obj.position.distanceToSQ(toPosition); + if(dist > REJUMP_MIN_DIST * REJUMP_MIN_DIST) { + double avail = usableFTL(ai, ord); + double cost = hyperdriveCost(ord.obj, toPosition); + if(avail >= cost && shouldHD(ord.obj, toPosition, ord.priority)) { + cast(ai.movement).order(ord); + return F_Continue; + } + } + } + } + return F_Pass; + } + + void focusTick(double time) override { + //Try to get enough ftl storage that we can ftl our largest fleet a fair distance and have some remaining + double highestCost = 0.0; + for(uint i = 0, cnt = fleets.fleets.length; i < cnt; ++i) { + auto@ flAI = fleets.fleets[i]; + if(flAI.fleetClass != FC_Combat) + continue; + vec3d toPosition = flAI.obj.position + vec3d(0, 0, STORAGE_AIM_DISTANCE); + highestCost = max(highestCost, double(hyperdriveCost(flAI.obj, toPosition))); + } + development.aimFTLStorage = highestCost / (1.0 - ai.behavior.ftlReservePctCritical - ai.behavior.ftlReservePctNormal); + } +}; + +AIComponent@ createHyperdrive() { + return Hyperdrive(); +} ADDED scripts/server/empire_ai/weasel/ftl/Jumpdrive.as Index: scripts/server/empire_ai/weasel/ftl/Jumpdrive.as ================================================================== --- scripts/server/empire_ai/weasel/ftl/Jumpdrive.as +++ scripts/server/empire_ai/weasel/ftl/Jumpdrive.as @@ -0,0 +1,208 @@ +import empire_ai.weasel.WeaselAI; +import empire_ai.weasel.Movement; +import empire_ai.weasel.Development; +import empire_ai.weasel.Fleets; + +import ftl; +import system_flags; +import regions.regions; +import systems; + +from orders import OrderType; + +const double REJUMP_MIN_DIST = 8000.0; + +class Jumpdrive : FTL { + Development@ development; + Fleets@ fleets; + + int safetyFlag = -1; + array safeRegions; + + void create() override { + @development = cast(ai.development); + @fleets = cast(ai.fleets); + + safetyFlag = getSystemFlag("JumpdriveSafety"); + } + + void save(SaveFile& file) { + uint cnt = safeRegions.length; + file << cnt; + for(uint i = 0; i < cnt; ++i) + file << safeRegions[i]; + } + + void load(SaveFile& file) { + uint cnt = 0; + file >> cnt; + safeRegions.length = cnt; + for(uint i = 0; i < cnt; ++i) + file >> safeRegions[i]; + } + + double jdETA(Object& obj, const vec3d& position) { + double charge = JUMPDRIVE_CHARGE_TIME; + return charge; + } + + double subETA(Object& obj, const vec3d& position) { + return newtonArrivalTime(obj.maxAcceleration, position - obj.position, vec3d()); + } + + bool shouldJD(Object& obj, const vec3d& position, uint priority) { + //This makes me sad + if(position.distanceTo(obj.position) < 3000) + return false; + return true; + + /*double factor = 0.8;*/ + /*if(priority == MP_Critical)*/ + /* factor = 1.0;*/ + /*return jdETA(obj, position) <= factor * subETA(obj, position);*/ + } + + uint order(MoveOrder& ord) override { + return order(ord, ord.obj.position, false); + } + + uint order(MoveOrder& ord, const vec3d& fromPos, bool secondary) { + if(!canJumpdrive(ord.obj)) + return F_Pass; + + double avail = usableFTL(ai, ord); + if(avail > 0) { + vec3d toPosition; + if(targetPosition(ord, toPosition)) { + double maxRange = jumpdriveRange(ord.obj); + double dist = toPosition.distanceTo(fromPos); + + bool isSafe = false; + Region@ reg = getRegion(toPosition); + if(reg !is null) + isSafe = reg.getSystemFlag(ai.empire, safetyFlag); + + if(dist > maxRange && !isSafe) { + //See if we should jump to a safe region first + if(!secondary) { + double bestHop = INFINITY; + Region@ hopRegion; + vec3d bestPos; + for(uint i = 0, cnt = safeRegions.length; i < cnt; ++i) { + if(!safeRegions[i].getSystemFlag(ai.empire, safetyFlag)) + continue; + vec3d hopPos = safeRegions[i].position; + hopPos = hopPos + (fromPos-hopPos).normalized(safeRegions[i].radius * 0.85); + double d = hopPos.distanceTo(toPosition); + if(d < bestHop) { + bestHop = d; + @hopRegion = safeRegions[i]; + bestPos = hopPos; + } + } + + if(bestHop < dist * 0.8) { + double cost = jumpdriveCost(ord.obj, fromPos, bestPos); + if(avail >= cost) { + ord.obj.addJumpdriveOrder(bestPos); + order(ord, bestPos, true); + return F_Continue; + } + } + } + + //Shorten our jump + if(ord.priority < MP_Normal) + return F_Pass; + toPosition = fromPos + (toPosition - fromPos).normalized(maxRange); + } + + if(shouldJD(ord.obj, toPosition, ord.priority)) { + double cost = jumpdriveCost(ord.obj, toPosition); + if(avail >= cost) { + ord.obj.addJumpdriveOrder(toPosition, append=secondary); + return F_Continue; + } + } + } + } + + return F_Pass; + } + + uint tick(MoveOrder& ord, double time) { + if(ord.priority == MP_Critical && canJumpdrive(ord.obj) && ord.obj.firstOrderType != OT_Jumpdrive) { + vec3d toPosition; + if(targetPosition(ord, toPosition)) { + double dist = ord.obj.position.distanceToSQ(toPosition); + if(dist > REJUMP_MIN_DIST * REJUMP_MIN_DIST) { + double maxRange = jumpdriveRange(ord.obj); + dist = sqrt(dist); + + bool isSafe = false; + Region@ reg = getRegion(toPosition); + if(reg !is null) + isSafe = reg.getSystemFlag(ai.empire, safetyFlag); + + if(dist > maxRange && !isSafe) + toPosition = ord.obj.position + (toPosition - ord.obj.position).normalized(maxRange); + + if(shouldJD(ord.obj, toPosition, ord.priority)) { + double avail = usableFTL(ai, ord); + double cost = jumpdriveCost(ord.obj, toPosition); + if(avail >= cost) { + cast(ai.movement).order(ord); + return F_Continue; + } + } + } + } + } + return F_Pass; + } + + uint sysChk = 0; + void start() { + for(uint i = 0, cnt = systemCount; i < cnt; ++i) { + Region@ reg = getSystem(i).object; + if(reg.getSystemFlag(ai.empire, safetyFlag)) + safeRegions.insertLast(reg); + } + } + + void focusTick(double time) override { + //Try to get enough ftl storage that we can ftl our largest fleet a fair distance and have some remaining + double highestCost = 0.0; + for(uint i = 0, cnt = fleets.fleets.length; i < cnt; ++i) { + auto@ flAI = fleets.fleets[i]; + if(flAI.fleetClass != FC_Combat) + continue; + double dist = jumpdriveRange(flAI.obj); + vec3d toPosition = flAI.obj.position + vec3d(0, 0, dist); + highestCost = max(highestCost, double(jumpdriveCost(flAI.obj, toPosition))); + } + development.aimFTLStorage = highestCost / (1.0 - ai.behavior.ftlReservePctCritical - ai.behavior.ftlReservePctNormal); + + //Disable systems that are no longer safe + for(uint i = 0, cnt = safeRegions.length; i < cnt; ++i) { + if(!safeRegions[i].getSystemFlag(ai.empire, safetyFlag)) { + safeRegions.removeAt(i); + --i; --cnt; + } + } + + //Try to find regions that are safe for us + { + sysChk = (sysChk+1) % systemCount; + auto@ reg = getSystem(sysChk).object; + if(reg.getSystemFlag(ai.empire, safetyFlag)) { + if(safeRegions.find(reg) == -1) + safeRegions.insertLast(reg); + } + } + } +}; + +AIComponent@ createJumpdrive() { + return Jumpdrive(); +} ADDED scripts/server/empire_ai/weasel/ftl/Slipstream.as Index: scripts/server/empire_ai/weasel/ftl/Slipstream.as ================================================================== --- scripts/server/empire_ai/weasel/ftl/Slipstream.as +++ scripts/server/empire_ai/weasel/ftl/Slipstream.as @@ -0,0 +1,384 @@ +import empire_ai.weasel.WeaselAI; +import empire_ai.weasel.Movement; +import empire_ai.weasel.Military; +import empire_ai.weasel.Construction; +import empire_ai.weasel.Designs; +import empire_ai.weasel.Development; +import empire_ai.weasel.Systems; +import empire_ai.weasel.Budget; +import empire_ai.weasel.Fleets; + +from statuses import getStatusID; +from abilities import getAbilityID; + +from oddity_navigation import hasOddityLink; + +const double SS_MIN_DISTANCE_STAGE = 0; +const double SS_MIN_DISTANCE_DEVELOP = 10000; +const double SS_MIN_TIMER = 3.0 * 60.0; +const double SS_MAX_DISTANCE = 3000.0; + +class SSRegion : Savable { + Region@ region; + Object@ obj; + bool arrived = false; + vec3d destination; + + void save(SaveFile& file) { + file << region; + file << obj; + file << arrived; + file << destination; + } + + void load(SaveFile& file) { + file >> region; + file >> obj; + file >> arrived; + file >> destination; + } +}; + +class Slipstream : FTL { + Military@ military; + Designs@ designs; + Construction@ construction; + Development@ development; + Systems@ systems; + Budget@ budget; + Fleets@ fleets; + + DesignTarget@ ssDesign; + + array tracked; + array unassigned; + + BuildFlagship@ buildSS; + double nextBuildTry = 15.0 * 60.0; + + void create() override { + @military = cast(ai.military); + @designs = cast(ai.designs); + @construction = cast(ai.construction); + @development = cast(ai.development); + @systems = cast(ai.systems); + @budget = cast(ai.budget); + @fleets = cast(ai.fleets); + } + + void save(SaveFile& file) override { + designs.saveDesign(file, ssDesign); + construction.saveConstruction(file, buildSS); + file << nextBuildTry; + + uint cnt = tracked.length; + file << cnt; + for(uint i = 0; i < cnt; ++i) + file << tracked[i]; + + cnt = unassigned.length; + file << cnt; + for(uint i = 0; i < cnt; ++i) + file << unassigned[i]; + } + + void load(SaveFile& file) override { + @ssDesign = designs.loadDesign(file); + @buildSS = cast(construction.loadConstruction(file)); + file >> nextBuildTry; + + uint cnt = 0; + file >> cnt; + for(uint i = 0; i < cnt; ++i) { + SSRegion gt; + file >> gt; + tracked.insertLast(gt); + } + + file >> cnt; + for(uint i = 0; i < cnt; ++i) { + Object@ obj; + file >> obj; + if(obj !is null) + unassigned.insertLast(obj); + } + } + + uint order(MoveOrder& ord) override { + //Find the position to fling to + vec3d toPosition; + if(!targetPosition(ord, toPosition)) + return F_Pass; + + //Check if we have a slipstream generator in this region + auto@ gt = get(ord.obj.region); + if(gt is null || gt.obj is null || !gt.arrived) + return F_Pass; + + //Make sure our generator is usable + Object@ ssGen = gt.obj; + if(!canSlipstream(ssGen)) + return F_Pass; + + //Check if we already have a link + if(hasOddityLink(gt.region, toPosition, SS_MAX_DISTANCE, minDuration=60.0)) + return F_Pass; + + //See if we have the FTL to make a link + double avail = usableFTL(ai, ord); + if(!canSlipstreamTo(ssGen, toPosition)) + return F_Pass; + if(slipstreamCost(ssGen, 0, toPosition.distanceTo(ssGen.position)) >= avail) + return F_Pass; + + ssGen.addSlipstreamOrder(toPosition, append=true); + if(ssGen !is ord.obj) { + ord.obj.addWaitOrder(ssGen, moveTo=true); + ssGen.addSecondaryToSlipstream(ord.obj); + } + else { + ord.obj.addMoveOrder(toPosition, append=true); + } + + return F_Continue; + } + + SSRegion@ get(Region@ reg) { + for(uint i = 0, cnt = tracked.length; i < cnt; ++i) { + if(tracked[i].region is reg) + return tracked[i]; + } + return null; + } + + void remove(SSRegion@ gt) { + if(gt.obj !is null && gt.obj.valid && gt.obj.owner is ai.empire) + unassigned.insertLast(gt.obj); + tracked.remove(gt); + } + + Object@ getClosest(const vec3d& position) { + Object@ closest; + double minDist = INFINITY; + for(uint i = 0, cnt = tracked.length; i < cnt; ++i) { + Object@ obj = tracked[i].obj; + if(obj is null) + continue; + if(!tracked[i].arrived) + continue; + double d = obj.position.distanceTo(position); + if(d < minDist) { + minDist = d; + @closest = obj; + } + } + return closest; + } + + SSRegion@ getClosestRegion(const vec3d& position) { + SSRegion@ closest; + double minDist = INFINITY; + for(uint i = 0, cnt = tracked.length; i < cnt; ++i) { + double d = tracked[i].region.position.distanceTo(position); + if(d < minDist) { + minDist = d; + @closest = tracked[i]; + } + } + return closest; + } + + void assignTo(SSRegion@ gt, Object@ closest) { + unassigned.remove(closest); + @gt.obj = closest; + gt.arrived = false; + military.stationFleet(fleets.getAI(closest), gt.region); + + if(closest.region is gt.region) + gt.arrived = true; + + if(!gt.arrived) { + gt.destination = military.getStationPosition(gt.region); + closest.addMoveOrder(gt.destination); + } + } + + bool trackingGen(Object@ obj) { + for(uint i = 0, cnt = unassigned.length; i < cnt; ++i) { + if(unassigned[i] is obj) + return true; + } + for(uint i = 0, cnt = tracked.length; i < cnt; ++i) { + if(tracked[i].obj is obj) + return true; + } + return false; + } + + bool shouldHaveGen(Region@ reg, bool always = false) { + if(military.getBase(reg) !is null) + return true; + if(development.isDevelopingIn(reg)) + return true; + return false; + } + + void turn() override { + if(ssDesign !is null && ssDesign.active !is null) { + int newSize = round(double(budget.spendable(BT_Military)) * 0.2 * ai.behavior.shipSizePerMoney / 64.0) * 64; + if(newSize < 128) + newSize = 128; + if(newSize != ssDesign.targetSize) { + @ssDesign = designs.design(DP_Slipstream, newSize); + ssDesign.customName = "Slipstream"; + } + } + } + + void focusTick(double time) override { + if(ai.behavior.forbidConstruction) return; + + //Design a generator + if(ssDesign is null) { + @ssDesign = designs.design(DP_Slipstream, 128); + ssDesign.customName = "Slipstream"; + } + + //Manage unassigned gens list + for(uint i = 0, cnt = unassigned.length; i < cnt; ++i) { + Object@ obj = unassigned[i]; + if(obj is null || !obj.valid || obj.owner !is ai.empire) { + unassigned.removeAt(i); + --i; --cnt; + } + } + + //Detect new gens + for(uint i = 0, cnt = fleets.fleets.length; i < cnt; ++i) { + auto@ flAI = fleets.fleets[i]; + if(flAI.fleetClass != FC_Slipstream) + continue; + if(!trackingGen(flAI.obj)) + unassigned.insertLast(flAI.obj); + } + + //Update existing gens for staging bases + for(uint i = 0, cnt = tracked.length; i < cnt; ++i) { + auto@ gt = tracked[i]; + bool checkAlways = false; + if(gt.obj !is null) { + if(!gt.obj.valid || gt.obj.owner !is ai.empire || (gt.arrived && gt.obj.region !is gt.region)) { + @gt.obj = null; + gt.arrived = false; + checkAlways = true; + } + else if(!gt.arrived && !gt.obj.hasOrders) { + if(gt.destination.distanceTo(gt.obj.position) < 10.0) + gt.arrived = true; + else + assignTo(gt, gt.obj); + } + } + if(!shouldHaveGen(gt.region, checkAlways)) { + remove(tracked[i]); + --i; --cnt; + } + } + + //Detect new staging bases to build gens at + for(uint i = 0, cnt = military.stagingBases.length; i < cnt; ++i) { + auto@ base = military.stagingBases[i]; + if(base.occupiedTime < SS_MIN_TIMER) + continue; + + if(get(base.region) is null) { + SSRegion@ closest = getClosestRegion(base.region.position); + if(closest !is null && closest.region.position.distanceTo(base.region.position) < SS_MIN_DISTANCE_STAGE) + continue; + + SSRegion gt; + @gt.region = base.region; + tracked.insertLast(gt); + break; + } + } + + //Detect new important planets to build generator at + for(uint i = 0, cnt = development.focuses.length; i < cnt; ++i) { + auto@ focus = development.focuses[i]; + Region@ reg = focus.obj.region; + if(reg is null) + continue; + + if(get(reg) is null) { + SSRegion@ closest = getClosestRegion(reg.position); + if(closest !is null && closest.region.position.distanceTo(reg.position) < SS_MIN_DISTANCE_DEVELOP) + continue; + + SSRegion gt; + @gt.region = reg; + tracked.insertLast(gt); + break; + } + } + + //See if we should build a new generator + if(buildSS !is null) { + if(buildSS.completed) { + @buildSS = null; + nextBuildTry = gameTime + 60.0; + } + } + for(uint i = 0, cnt = tracked.length; i < cnt; ++i) { + auto@ gt = tracked[i]; + if(gt.obj is null && gt.region.ContestedMask & ai.mask == 0 && gt.region.BlockFTLMask & ai.mask == 0) { + Object@ closest; + double closestDist = INFINITY; + for(uint n = 0, ncnt = unassigned.length; n < ncnt; ++n) { + Object@ obj = unassigned[n]; + if(obj.region is gt.region) { + @closest = obj; + break; + } + if(!obj.hasMover) + continue; + if(buildSS is null && gameTime > nextBuildTry) { + double d = obj.position.distanceTo(gt.region.position); + if(d < closestDist) { + closestDist = d; + @closest = obj; + } + } + } + + if(closest !is null) { + if(log) + ai.print("Assign slipstream gen to => "+gt.region.name, closest.region); + assignTo(gt, closest); + } else if(buildSS is null && gameTime > nextBuildTry) { + if(log) + ai.print("Build slipstream gen for this system", gt.region); + + @buildSS = construction.buildFlagship(ssDesign); + } + } + } + + //Try to get enough ftl storage that we can permanently open a slipstream with each of generators + double mostCost = 0.0; + for(uint i = 0, cnt = tracked.length; i < cnt; ++i) { + Ship@ obj = cast(tracked[i].obj); + if(obj is null) + continue; + + double baseCost = obj.blueprint.design.average(SV_SlipstreamCost); + double duration = obj.blueprint.design.average(SV_SlipstreamDuration); + mostCost += baseCost / duration; + } + development.aimFTLStorage = mostCost; + } +}; + +AIComponent@ createSlipstream() { + return Slipstream(); +} ADDED scripts/server/empire_ai/weasel/misc/Invasion.as Index: scripts/server/empire_ai/weasel/misc/Invasion.as ================================================================== --- scripts/server/empire_ai/weasel/misc/Invasion.as +++ scripts/server/empire_ai/weasel/misc/Invasion.as @@ -0,0 +1,349 @@ +import empire_ai.weasel.WeaselAI; + +import empire_ai.weasel.Fleets; +import empire_ai.weasel.Systems; +import empire_ai.weasel.Movement; +import empire_ai.weasel.searches; + +import systems; +from empire import Pirates; + +class InvasionDefendMission : Mission { + FleetAI@ fleet; + Region@ targRegion; + MoveOrder@ move; + bool pending = false; + + Object@ eliminate; + + void save(Fleets& fleets, SaveFile& file) override { + fleets.saveAI(file, fleet); + file << targRegion; + fleets.movement.saveMoveOrder(file, move); + file << pending; + } + + void load(Fleets& fleets, SaveFile& file) override { + @fleet = fleets.loadAI(file); + file >> targRegion; + @move = fleets.movement.loadMoveOrder(file); + file >> pending; + } + + bool get_isActive() override { + return targRegion !is null; + } + + void tick(AI& ai, FleetAI& fleet, double time) override { + if(targRegion is null) + return; + if(move !is null) + return; + + //Find stuff to fight + if(eliminate is null) + @eliminate = findEnemy(targRegion, null, ai.empire.hostileMask); + + if(eliminate !is null) { + if(!eliminate.valid) { + @eliminate = null; + } + else { + if(!fleet.obj.hasOrders) + fleet.obj.addAttackOrder(eliminate); + if((fleet.filled < 0.3 || fleet.supplies < 0.3 || fleet.flagshipHealth < 0.5) + && eliminate.getFleetStrength() * 2.0 > fleet.strength + && !pending) { + @targRegion = null; + @eliminate = null; + @move = cast(ai.fleets).returnToBase(fleet, MP_Critical); + } + } + } + } + + void update(AI& ai, Invasion& invasion) { + //Manage movement + if(move !is null) { + if(move.failed || move.completed) + @move = null; + } + + //Find new regions to go to + if(targRegion is null || (!pending && move is null && !invasion.isFighting(targRegion))) { + bool ready = fleet.actionableState && move is null; + + DefendSystem@ bestDef; + double bestWeight = 0.0; + + for(uint i = 0, cnt = invasion.defending.length; i < cnt; ++i) { + auto@ def = invasion.defending[i]; + double w = randomd(0.9, 1.1); + if(!def.fighting) { + if(!ready) + continue; + else + w *= 0.1; + } + + if(!def.winning) { + w *= 10.0; + } + else { + if(!ready) + continue; + } + + if(def.obj is targRegion) + w *= 1.5; + + if(w > bestWeight) { + bestWeight = w; + @bestDef = def; + } + } + + if(bestDef !is null && fleet.supplies >= 0.25 && fleet.filled >= 0.2 && fleet.fleetHealth >= 0.2) { + @targRegion = bestDef.obj; + invasion.pend(targRegion, fleet); + pending = true; + } + } + + //Move to the region we want to go to + if(targRegion !is null) { + if(move is null) { + if(fleet.obj.region !is targRegion) { + @eliminate = findEnemy(targRegion, null, ai.empire.hostileMask); + if(eliminate is null) { + vec3d targPos = targRegion.position; + targPos += (targRegion.position - ai.empire.HomeSystem.position).normalized(targRegion.radius * 0.85); + + @move = invasion.movement.move(fleet.obj, targPos, MP_Critical); + } + else { + @move = invasion.movement.move(fleet.obj, eliminate, MP_Critical, nearOnly=true); + } + } + else { + //Remove from pending list + if(pending) { + invasion.unpend(targRegion, fleet); + pending = false; + } + + //See if we should return to base + if(!invasion.isFighting(targRegion) && (fleet.supplies < 0.25 || fleet.filled < 0.5)) { + @targRegion = null; + @move = invasion.fleets.returnToBase(fleet, MP_Critical); + } + } + } + } + } +}; + +class DefendSystem { + Region@ obj; + array pending; + + double enemyStrength = 0.0; + double ourStrength = 0.0; + double remnantStrength = 0.0; + double pendingStrength = 0.0; + + void save(Invasion& invasion, SaveFile& file) { + file << obj; + + uint cnt = pending.length; + file << cnt; + for(uint i = 0; i < cnt; ++i) + invasion.fleets.saveAI(file, pending[i]); + + file << enemyStrength; + file << ourStrength; + file << remnantStrength; + file << pendingStrength; + } + + void load(Invasion& invasion, SaveFile& file) { + file >> obj; + + uint cnt = 0; + file >> cnt; + for(uint i = 0; i < cnt; ++i) { + auto@ fleet = invasion.fleets.loadAI(file); + if(fleet !is null && fleet.obj !is null) + pending.insertLast(fleet); + } + + file >> enemyStrength; + file >> ourStrength; + file >> remnantStrength; + file >> pendingStrength; + } + + void update(AI& ai, Invasion& invasion) { + enemyStrength = getTotalFleetStrength(obj, ai.empire.hostileMask); + + ourStrength = getTotalFleetStrength(obj, ai.mask); + remnantStrength = getTotalFleetStrength(obj, Pirates.mask); + if(gameTime < 10.0 * 60.0) + ourStrength += remnantStrength; + else if(gameTime < 30.0 * 60.0) + ourStrength += remnantStrength * 0.5; + + pendingStrength = 0.0; + for(uint i = 0, cnt = pending.length; i < cnt; ++i) + pendingStrength += sqrt(pending[i].strength); + pendingStrength *= pendingStrength; + + if(obj.PlanetsMask & ai.empire.mask != 0) + ai.empire.setDefending(obj, true); + } + + bool get_fighting() { + return enemyStrength > 0; + } + + bool get_winning() { + return ourStrength + pendingStrength > enemyStrength; + } +}; + +class Invasion : AIComponent { + Fleets@ fleets; + Movement@ movement; + + array defending; + array tracked; + + void create() { + @fleets = cast(ai.fleets); + @movement = cast(ai.movement); + + ai.behavior.maintenancePerShipSize = 0.0; + } + + void save(SaveFile& file) { + uint cnt = defending.length; + file << cnt; + for(uint i = 0; i < cnt; ++i) + defending[i].save(this, file); + + cnt = tracked.length; + file << cnt; + for(uint i = 0; i < cnt; ++i) + fleets.saveMission(file, tracked[i]); + } + + void load(SaveFile& file) { + uint cnt = 0; + + file >> cnt; + for(uint i = 0; i < cnt; ++i) { + DefendSystem def; + def.load(this, file); + defending.insertLast(def); + } + + file >> cnt; + for(uint i = 0; i < cnt; ++i) { + InvasionDefendMission@ miss = cast(fleets.loadMission(file)); + if(miss !is null) + tracked.insertLast(miss); + } + } + + void start() { + //Find systems to defend + Region@ home = ai.empire.HomeSystem; + const SystemDesc@ sys = getSystem(home); + for(uint i = 0, cnt = sys.adjacent.length; i < cnt; ++i) { + auto@ otherSys = getSystem(sys.adjacent[i]); + if(findEnemy(otherSys.object, null, Pirates.mask, fleets=false, stations=true) !is null) { + DefendSystem def; + @def.obj = otherSys.object; + defending.insertLast(def); + } + } + } + + bool isManaging(FleetAI& fleet) { + if(fleet.mission is null) + return false; + for(uint i = 0, cnt = tracked.length; i < cnt; ++i) { + if(tracked[i] is fleet.mission) + return true; + } + return false; + } + + void manage(FleetAI& fleet) { + InvasionDefendMission miss; + @miss.fleet = fleet; + + fleets.performMission(fleet, miss); + tracked.insertLast(miss); + } + + void pend(Region@ region, FleetAI& fleet) { + for(uint i = 0, cnt = defending.length; i < cnt; ++i ){ + if(defending[i].obj is region) { + defending[i].pending.insertLast(fleet); + break; + } + } + } + + void unpend(Region@ region, FleetAI& fleet) { + for(uint i = 0, cnt = defending.length; i < cnt; ++i ){ + if(defending[i].obj is region) { + defending[i].pending.remove(fleet); + break; + } + } + } + + DefendSystem@ getDefending(Region@ region) { + for(uint i = 0, cnt = defending.length; i < cnt; ++i ){ + if(defending[i].obj is region) + return defending[i]; + } + return null; + } + + bool isFighting(Region@ region) { + for(uint i = 0, cnt = defending.length; i < cnt; ++i ){ + if(defending[i].obj is region) + return defending[i].fighting; + } + return false; + } + + uint sysUpd = 0; + void focusTick(double time) { + //All your fleets are belong to us + for(uint i = 0, cnt = fleets.fleets.length; i < cnt; ++i) { + auto@ flAI = fleets.fleets[i]; + if(flAI.fleetClass != FC_Combat) + continue; + if(!isManaging(flAI)) + manage(flAI); + } + + //Update systems we're defending + if(defending.length != 0) { + sysUpd = (sysUpd+1) % defending.length; + defending[sysUpd].update(ai, this); + } + + //Make sure our fleets are in the right places + for(uint i = 0, cnt = tracked.length; i < cnt; ++i) + tracked[i].update(ai, this); + } +}; + +AIComponent@ createInvasion() { + return Invasion(); +} ADDED scripts/server/empire_ai/weasel/race/Ancient.as Index: scripts/server/empire_ai/weasel/race/Ancient.as ================================================================== --- scripts/server/empire_ai/weasel/race/Ancient.as +++ scripts/server/empire_ai/weasel/race/Ancient.as @@ -0,0 +1,520 @@ +import empire_ai.weasel.WeaselAI; +import empire_ai.weasel.race.Race; + +import empire_ai.weasel.Colonization; +import empire_ai.weasel.Construction; +import empire_ai.weasel.Resources; +import empire_ai.weasel.Development; +import empire_ai.weasel.Movement; +import empire_ai.weasel.Planets; +import empire_ai.weasel.Orbitals; + +from orbitals import getOrbitalModule, OrbitalModule; +from buildings import getBuildingType, BuildingType; +from resources import ResourceType, getResource, getResourceID; +from statuses import getStatusID; +from biomes import getBiomeID; + +enum PlanetClass { + PC_Empty, + PC_Core, + PC_Mine, + PC_Transmute, +} + +class TrackReplicator { + Object@ obj; + Planet@ target; + bool arrived = false; + MoveOrder@ move; + BuildingRequest@ build; + uint intention = PC_Empty; + + bool get_busy() { + if(target is null) + return false; + if(!arrived || move !is null || build !is null) + return true; + return false; + } + + void save(Ancient& ancient, SaveFile& file) { + file << obj; + file << target; + file << arrived; + ancient.movement.saveMoveOrder(file, move); + ancient.planets.saveBuildingRequest(file, build); + file << intention; + } + + void load(Ancient& ancient, SaveFile& file) { + file >> obj; + file >> target; + file >> arrived; + @move = ancient.movement.loadMoveOrder(file); + @build = ancient.planets.loadBuildingRequest(file); + file >> intention; + } +}; + +class Ancient : Race, RaceResources, RaceColonization { + Colonization@ colonization; + Construction@ construction; + Resources@ resources; + Planets@ planets; + Development@ development; + Movement@ movement; + Orbitals@ orbitals; + + array replicators; + + const OrbitalModule@ replicatorMod; + + const BuildingType@ core; + const BuildingType@ miner; + const BuildingType@ transmuter; + + const BuildingType@ foundry; + + const BuildingType@ depot; + const BuildingType@ refinery; + const BuildingType@ reinforcer; + const BuildingType@ developer; + const BuildingType@ compressor; + + int claimStatus = -1; + int replicatorStatus = -1; + + int mountainsBiome = -1; + + int oreResource = -1; + int baseMatResource = -1; + + bool foundFirstT2 = false; + + void create() { + @colonization = cast(ai.colonization); + colonization.performColonization = false; + + @resources = cast(ai.resources); + @construction = cast(ai.construction); + @movement = cast(ai.movement); + @planets = cast(ai.planets); + @orbitals = cast(ai.orbitals); + @planets = cast(ai.planets); + + @development = cast(ai.development); + development.managePlanetPressure = false; + development.buildBuildings = false; + development.colonizeResources = false; + + @replicatorMod = getOrbitalModule("AncientReplicator"); + + @transmuter = getBuildingType("AncientTransmuter"); + @miner = getBuildingType("AncientMiner"); + @core = getBuildingType("AncientCore"); + + @foundry = getBuildingType("AncientFoundry"); + + @depot = getBuildingType("AncientDepot"); + @refinery = getBuildingType("AncientRefinery"); + @reinforcer = getBuildingType("AncientReinforcer"); + @developer = getBuildingType("AncientDeveloper"); + @compressor = getBuildingType("Compressor"); + + claimStatus = getStatusID("AncientClaim"); + replicatorStatus = getStatusID("AncientReplicator"); + + mountainsBiome = getBiomeID("Mountains"); + + oreResource = getResourceID("OreRate"); + baseMatResource = getResourceID("BaseMaterial"); + + @ai.defs.Factory = null; + @ai.defs.LaborStorage = null; + } + + void save(SaveFile& file) override { + file << foundFirstT2; + uint cnt = replicators.length; + file << cnt; + for(uint i = 0; i < cnt; ++i) + replicators[i].save(this, file); + } + + void load(SaveFile& file) override { + file >> foundFirstT2; + uint cnt = 0; + file >> cnt; + for(uint i = 0; i < cnt; ++i) { + TrackReplicator t; + t.load(this, file); + if(t.obj !is null) + replicators.insertLast(t); + } + } + + void levelRequirements(Object& obj, int targetLevel, array& specs) { + //YOLO + specs.length = 0; + } + + bool orderColonization(ColonizeData& data, Planet@ sourcePlanet) { + return true; + } + + double getGenericUsefulness(const ResourceType@ type) { + return 1.0; + } + + bool hasReplicator(Planet& pl) { + for(uint i = 0, cnt = replicators.length; i < cnt; ++i) { + if(replicators[i].target is pl) + return true; + } + return false; + } + + bool isTracking(Object& obj) { + for(uint i = 0, cnt = replicators.length; i < cnt; ++i) { + if(replicators[i].obj is obj) + return true; + } + return false; + } + + void trackReplicator(Object& obj) { + TrackReplicator t; + @t.obj = obj; + + replicators.insertLast(t); + } + + void updateRequests(Planet& pl) { + //Handle requests for base materials + uint baseMatReqs = 0; + baseMatReqs += pl.getBuildingCount(depot.id); + baseMatReqs += pl.getBuildingCount(refinery.id); + baseMatReqs += pl.getBuildingCount(reinforcer.id); + baseMatReqs += pl.getBuildingCount(developer.id); + baseMatReqs += pl.getBuildingCount(compressor.id); + + array curBaseMat; + resources.getImportsOf(curBaseMat, baseMatResource, pl); + + if(curBaseMat.length < baseMatReqs) { + for(uint i = curBaseMat.length, cnt = baseMatReqs; i < cnt; ++i) { + ResourceSpec spec; + spec.type = RST_Specific; + @spec.resource = getResource(baseMatResource); + + resources.requestResource(pl, spec); + } + } + else if(curBaseMat.length > baseMatReqs) { + for(uint i = baseMatReqs, cnt = curBaseMat.length; i < cnt; ++i) + resources.cancelRequest(curBaseMat[i]); + } + + //Handle requests for ore + uint oreReqs = 0; + oreReqs += pl.getBuildingCount(foundry.id); + + array curOre; + resources.getImportsOf(curOre, oreResource, pl); + + if(curOre.length < oreReqs) { + for(uint i = curOre.length, cnt = oreReqs; i < cnt; ++i) { + ResourceSpec spec; + spec.type = RST_Specific; + @spec.resource = getResource(oreResource); + + resources.requestResource(pl, spec); + } + } + else if(curOre.length > oreReqs) { + for(uint i = oreReqs, cnt = curOre.length; i < cnt; ++i) + resources.cancelRequest(curOre[i]); + } + } + + uint plInd = 0; + void focusTick(double time) { + if(ai.behavior.forbidColonization) return; + + //Find new replicators + for(uint i = 0, cnt = orbitals.orbitals.length; i < cnt; ++i) { + auto@ orb = cast(orbitals.orbitals[i].obj); + if(orb.coreModule == replicatorMod.id) { + if(!isTracking(orb)) + trackReplicator(orb); + } + } + + //Update requests for planets + if(planets.planets.length != 0) { + for(uint n = 0, ncnt = min(planets.planets.length, 10); n < ncnt; ++n) { + plInd = (plInd+1) % planets.planets.length; + Planet@ pl = planets.planets[plInd].obj; + + if(classify(pl) == PC_Core) + updateRequests(pl); + } + } + + //Manage existing replicators + for(uint i = 0, cnt = replicators.length; i < cnt; ++i) { + auto@ t = replicators[i]; + if(t.obj is null || !t.obj.valid || t.obj.owner !is ai.empire) { + replicators.removeAt(i); + --i; --cnt; + continue; + } + + if(t.target !is null) { + if(!t.target.valid) { + @t.target = null; + if(!t.arrived) + t.obj.stopMoving(); + t.arrived = false; + } + else if(t.target.owner !is ai.empire && t.target.owner.valid) { + @t.target = null; + if(!t.arrived) + t.obj.stopMoving(); + t.arrived = false; + } + } + + if(t.move !is null) { + if(t.move.failed) { + @t.move = null; + t.arrived = false; + } + else if(t.move.completed) { + if(t.obj.isOrbitingAround(t.target)) { + @t.move = null; + t.arrived = true; + } + else if(t.obj.inOrbit) { + @t.move = null; + t.arrived = false; + @t.target = null; + } + } + } + else if(t.target !is null && !t.arrived) { + @t.move = movement.move(t.obj, t.target); + } + + if(t.build !is null) { + if(t.build.canceled) { + //A build failed, give up on this planet + if(log) + ai.print("Failed building build", t.target); + @t.target = null; + @t.build = null; + t.arrived = false; + } + else if(t.build.built) { + float progress = t.build.getProgress(); + if(progress >= 1.f) { + if(log) + ai.print("Completed building build", t.target); + @t.build = null; + } + else if(progress < -0.5f) { + if(log) + ai.print("Failed building build location "+t.build.builtAt, t.target); + @t.build = null; + @t.target = null; + t.arrived = false; + } + } + } + + if(t.arrived || t.target is null) { + if(!t.busy) + useReplicator(t); + } + } + } + + uint classify(Planet& pl) { + int resType = pl.primaryResourceType; + if(resType == oreResource) + return PC_Mine; + if(resType == baseMatResource) + return PC_Transmute; + uint claims = pl.getStatusStackCountAny(claimStatus); + if(claims <= 1) + return PC_Empty; + if(pl.getBuildingCount(core.id) >= 1) + return PC_Core; + if(pl.getBuildingCount(transmuter.id) >= 1) + return PC_Transmute; + if(pl.getBuildingCount(miner.id) >= 1) + return PC_Mine; + return PC_Empty; + } + + bool shouldBeCore(const ResourceType@ type) { + if(type.level >= 1) + return true; + if(type.totalPressure >= 8) + return true; + return false; + } + + int openOreRequests(TrackReplicator@ discount = null) { + int reqs = 0; + for(uint i = 0, cnt = resources.requested.length; i < cnt; ++i) { + auto@ req = resources.requested[i]; + if(req.beingMet) + continue; + if(req.spec.type != RST_Specific) + continue; + if(req.spec.resource.id != uint(oreResource)) + continue; + reqs += 1; + } + for(uint i = 0, cnt = replicators.length; i < cnt; ++i) { + auto@ t = replicators[i]; + if(t is discount) + continue; + if(t.target is null) + continue; + if(t.intention == PC_Mine && (t.build is null || t.build.type is miner)) + reqs -= 1; + } + return reqs; + } + + int openBaseMatRequests(TrackReplicator@ discount = null) { + int reqs = 0; + for(uint i = 0, cnt = resources.requested.length; i < cnt; ++i) { + auto@ req = resources.requested[i]; + if(req.beingMet) + continue; + if(req.spec.type != RST_Specific) + continue; + if(req.spec.resource.id != uint(baseMatResource)) + continue; + reqs += 1; + } + for(uint i = 0, cnt = replicators.length; i < cnt; ++i) { + auto@ t = replicators[i]; + if(t is discount) + continue; + if(t.target is null) + continue; + if(t.intention == PC_Transmute && (t.build is null || t.build.type is transmuter)) + reqs -= 1; + } + return reqs; + } + + void build(TrackReplicator& t, const BuildingType@ building) { + auto@ plAI = planets.getAI(t.target); + if(plAI is null) + return; + if(!t.target.hasStatusEffect(replicatorStatus)) + return; + + //bool scatter = building is miner || building is transmuter; + bool scatter = false; + @t.build = planets.requestBuilding(plAI, building, scatter=scatter, moneyType=BT_Colonization); + + if(log) + ai.print("Build "+building.name, t.target); + } + + void useReplicator(TrackReplicator& t) { + if(t.target !is null) { + uint type = classify(t.target); + switch(type) { + case PC_Empty: { + const ResourceType@ res = getResource(t.target.primaryResourceType); + if(res is null) { + @t.target = null; + t.arrived = false; + return; + } + + if(shouldBeCore(res)) { + build(t, core); + } + else if(openBaseMatRequests(t) >= openOreRequests(t) || gameTime < 6.0 * 60.0 || !t.target.hasBiome(mountainsBiome)) { + build(t, transmuter); + } + else { + build(t, miner); + } + return; + } + case PC_Transmute: + @t.target = null; + t.arrived = false; + break; + case PC_Mine: + @t.target = null; + t.arrived = false; + break; + case PC_Core: + build(t, refinery); + return; + } + } + + //Find a new planet to colonize + PotentialColonize@ best; + double bestWeight = 0.0; + + uint getType = PC_Core; + if(openBaseMatRequests() >= 1) + getType = PC_Transmute; + else if(openOreRequests() >= 1 && gameTime > 6.0 * 60.0) + getType = PC_Mine; + + auto@ potentials = colonization.getPotentialColonize(); + for(uint i = 0, cnt = potentials.length; i < cnt; ++i) { + PotentialColonize@ p = potentials[i]; + if(hasReplicator(p.pl)) + continue; + + double w = p.weight; + if(!foundFirstT2 && p.resource.level >= 2) + w *= 100.0; + else if((getType == PC_Core) != shouldBeCore(p.resource)) + w *= 0.6; + if(getType == PC_Core && p.resource.level >= 2) + w *= 4.0; + if(getType == PC_Core && p.resource.level >= 3) + w *= 6.0; + if(getType == PC_Mine && !p.pl.hasBiome(mountainsBiome)) + w *= 0.1; + if(getType == PC_Core) + w *= double(p.pl.totalSurfaceTiles) / 100.0; + w /= p.pl.position.distanceTo(t.obj.position)/1000.0; + + if(w > bestWeight) { + bestWeight = w; + @best = p; + } + } + + if(best !is null) { + @t.target = best.pl; + t.intention = shouldBeCore(best.resource) ? uint(PC_Core) : getType; + t.arrived = false; + if(!foundFirstT2) { + if(best.resource.level == 2) + foundFirstT2 = true; + } + } + } +}; + +AIComponent@ createAncient() { + return Ancient(); +} ADDED scripts/server/empire_ai/weasel/race/Devout.as Index: scripts/server/empire_ai/weasel/race/Devout.as ================================================================== --- scripts/server/empire_ai/weasel/race/Devout.as +++ scripts/server/empire_ai/weasel/race/Devout.as @@ -0,0 +1,152 @@ +import empire_ai.weasel.WeaselAI; +import empire_ai.weasel.race.Race; + +import empire_ai.weasel.Development; +import empire_ai.weasel.Planets; +import empire_ai.weasel.Budget; + +import resources; +import buildings; +import attributes; + +class Devout : Race, RaceDevelopment { + Development@ development; + Planets@ planets; + Budget@ budget; + + const ResourceType@ altarResource; + const BuildingType@ altar; + + int coverAttrib = -1; + + BuildingRequest@ altarBuild; + Planet@ focusAltar; + + double considerTimer = 0.0; + + void save(SaveFile& file) { + planets.saveBuildingRequest(file, altarBuild); + file << focusAltar; + file << considerTimer; + } + + void load(SaveFile& file) { + @altarBuild = planets.loadBuildingRequest(file); + file >> focusAltar; + file >> considerTimer; + } + + void create() { + @planets = cast(ai.planets); + @development = cast(ai.development); + @budget = cast(ai.budget); + + @altarResource = getResource("Altar"); + + @altar = getBuildingType("Altar"); + + coverAttrib = getEmpAttribute("AltarSupportedPopulation"); + } + + void start() { + auto@ data = ai.empire.getPlanets(); + Object@ obj; + while(receive(data, obj)) { + Planet@ pl = cast(obj); + if(pl !is null){ + if(pl.primaryResourceType == altarResource.id) { + @focusAltar = pl; + break; + } + } + } + } + + bool shouldBeFocus(Planet& pl, const ResourceType@ resource) override { + if(resource is altarResource) + return true; + return false; + } + + void focusTick(double time) override { + if(ai.behavior.forbidConstruction) return; + + //Handle our current altar build + if(altarBuild !is null) { + if(altarBuild.built) { + @focusAltar = altarBuild.plAI.obj; + @altarBuild = null; + } + else if(altarBuild.canceled) { + @altarBuild = null; + } + } + + //Handle our focused altar + if(focusAltar !is null) { + if(!focusAltar.valid || focusAltar.owner !is ai.empire || focusAltar.primaryResourceType != altarResource.id) { + @focusAltar = null; + } + } + + //If we aren't covering our entire population, find new planets to make into altars + double coverage = ai.empire.getAttribute(coverAttrib); + double population = ai.empire.TotalPopulation; + + if(coverage >= population || altarBuild !is null) + return; + + bool makeNewAltar = true; + if(focusAltar !is null) { + auto@ foc = development.getFocus(focusAltar); + if(foc !is null && int(foc.obj.level) >= foc.targetLevel) { + foc.targetLevel += 1; + considerTimer = gameTime + 180.0; + makeNewAltar = false; + } + else { + makeNewAltar = gameTime > considerTimer; + } + } + + if(makeNewAltar) { + if(budget.canSpend(BT_Development, 300)) { + //Turn our most suitable planet into an altar + PlanetAI@ bestBuild; + double bestWeight = 0.0; + + for(uint i = 0, cnt = planets.planets.length; i < cnt; ++i) { + auto@ plAI = planets.planets[i]; + double w = randomd(0.9, 1.1); + + if(plAI.resources !is null && plAI.resources.length != 0) { + auto@ res = plAI.resources[0].resource; + if(res.level == 0 && !res.limitlessLevel) + w *= 5.0; + if(res.cls !is null) + w *= 0.5; + if(res.level > 0) + w /= pow(2.0, res.level); + } + else { + w *= 100.0; + } + + if(w > bestWeight) { + bestWeight = w; + @bestBuild = plAI; + } + } + + if(bestBuild !is null) { + @altarBuild = planets.requestBuilding(bestBuild, altar, expire=60.0); + considerTimer = gameTime + 120.0; + } + } + } + } +}; + +AIComponent@ createDevout() { + return Devout(); +} ADDED scripts/server/empire_ai/weasel/race/Extragalactic.as Index: scripts/server/empire_ai/weasel/race/Extragalactic.as ================================================================== --- scripts/server/empire_ai/weasel/race/Extragalactic.as +++ scripts/server/empire_ai/weasel/race/Extragalactic.as @@ -0,0 +1,201 @@ +import empire_ai.weasel.WeaselAI; +import empire_ai.weasel.race.Race; + +import empire_ai.weasel.Colonization; +import empire_ai.weasel.Construction; +import empire_ai.weasel.Resources; +import empire_ai.weasel.Scouting; +import empire_ai.weasel.Orbitals; +import empire_ai.weasel.Budget; + +from orbitals import getOrbitalModuleID; +from constructions import ConstructionType, getConstructionType; + +class Extragalactic : Race { + Colonization@ colonization; + Construction@ construction; + Scouting@ scouting; + Orbitals@ orbitals; + Resources@ resources; + Budget@ budget; + + array beacons; + OrbitalAI@ masterBeacon; + + int beaconMod = -1; + + array imports; + array beaconBuilds; + + void create() { + @colonization = cast(ai.colonization); + colonization.performColonization = false; + colonization.queueColonization = false; + + @scouting = cast(ai.scouting); + scouting.buildScouts = false; + + @orbitals = cast(ai.orbitals); + beaconMod = getOrbitalModuleID("Beacon"); + + @construction = cast(ai.construction); + @resources = cast(ai.resources); + @budget = cast(ai.budget); + + beaconBuilds.insertLast(getConstructionType("BeaconHealth")); + beaconBuilds.insertLast(getConstructionType("BeaconWeapons")); + beaconBuilds.insertLast(getConstructionType("BeaconLabor")); + } + + void save(SaveFile& file) override { + uint cnt = beacons.length; + file << cnt; + for(uint i = 0; i < cnt; ++i) + orbitals.saveAI(file, beacons[i]); + orbitals.saveAI(file, masterBeacon); + + cnt = imports.length; + file << cnt; + for(uint i = 0; i < cnt; ++i) + resources.saveImport(file, imports[i]); + } + + void load(SaveFile& file) override { + uint cnt = 0; + file >> cnt; + for(uint i = 0; i < cnt; ++i) { + auto@ b = orbitals.loadAI(file); + if(b !is null && b.obj !is null) + beacons.insertLast(b); + } + @masterBeacon = orbitals.loadAI(file); + + file >> cnt; + for(uint i = 0; i < cnt; ++i) { + auto@ imp = resources.loadImport(file); + if(imp !is null) + imports.insertLast(imp); + } + } + + uint prevBeacons = 0; + void focusTick(double time) { + if(ai.behavior.forbidConstruction) return; + + //Find our beacons + for(uint i = 0, cnt = beacons.length; i < cnt; ++i) { + auto@ b = beacons[i]; + if(b is null || b.obj is null || !b.obj.valid || b.obj.owner !is ai.empire) { + if(b.obj !is null) + resources.killImportsTo(b.obj); + beacons.removeAt(i); + --i; --cnt; + } + } + + for(uint i = 0, cnt = orbitals.orbitals.length; i < cnt; ++i) { + auto@ orb = orbitals.orbitals[i]; + Orbital@ obj = cast(orb.obj); + if(obj !is null && obj.coreModule == uint(beaconMod)) { + if(beacons.find(orb) == -1) + beacons.insertLast(orb); + } + } + + //Find our master beacon + if(masterBeacon !is null) { + Orbital@ obj = cast(masterBeacon.obj); + if(obj is null || !obj.valid || obj.owner !is ai.empire || obj.hasMaster()) + @masterBeacon = null; + } + else { + for(uint i = 0, cnt = beacons.length; i < cnt; ++i) { + auto@ b = beacons[i]; + Orbital@ obj = cast(b.obj); + if(!obj.hasMaster()) { + @masterBeacon = b; + ai.empire.setDefending(obj, true); + break; + } + } + } + + scouting.buildScouts = gameTime > 5.0 * 60.0; + if(prevBeacons < beacons.length && masterBeacon !is null && gameTime > 10.0) { + for(int i = beacons.length-1; i >= int(prevBeacons); --i) { + //Make sure we order a scout at each beacon + if(!scouting.buildScouts) { + BuildFlagshipSourced build(scouting.scoutDesign); + build.moneyType = BT_Military; + @build.buildAt = masterBeacon.obj; + if(beacons[i] !is masterBeacon) + @build.buildFrom = beacons[i].obj; + + construction.build(build, force=true); + } + + //Set the beacon to fill up other stuff + beacons[i].obj.allowFillFrom = true; + } + prevBeacons = beacons.length; + } + + //Handle with importing labor and defense to our master beacon + if(masterBeacon !is null) { + if(imports.length == 0) { + //Request labor and defense at our beacon + { + ResourceSpec spec; + spec.type = RST_Pressure_Type; + spec.pressureType = TR_Labor; + + imports.insertLast(resources.requestResource(masterBeacon.obj, spec)); + } + { + ResourceSpec spec; + spec.type = RST_Pressure_Type; + spec.pressureType = TR_Defense; + + imports.insertLast(resources.requestResource(masterBeacon.obj, spec)); + } + { + ResourceSpec spec; + spec.type = RST_Pressure_Level0; + spec.pressureType = TR_Research; + + imports.insertLast(resources.requestResource(masterBeacon.obj, spec)); + } + } + else { + //When our requests are met, make more requests! + for(uint i = 0, cnt = imports.length; i < cnt; ++i) { + if(imports[i].beingMet || imports[i].obj !is masterBeacon.obj) { + ResourceSpec spec; + spec = imports[i].spec; + @imports[i] = resources.requestResource(masterBeacon.obj, spec); + } + } + } + + //Build stuff on our beacon if we have enough stuff + if(budget.canSpend(BT_Development, 300)) { + uint offset = randomi(0, beaconBuilds.length-1); + for(uint i = 0, cnt = beaconBuilds.length; i < cnt; ++i) { + uint ind = (i+offset) % cnt; + auto@ type = beaconBuilds[ind]; + if(type is null) + continue; + + if(type.canBuild(masterBeacon.obj, ignoreCost=false)) { + masterBeacon.obj.buildConstruction(type.id); + break; + } + } + } + } + } +}; + +AIComponent@ createExtragalactic() { + return Extragalactic(); +} ADDED scripts/server/empire_ai/weasel/race/Linked.as Index: scripts/server/empire_ai/weasel/race/Linked.as ================================================================== --- scripts/server/empire_ai/weasel/race/Linked.as +++ scripts/server/empire_ai/weasel/race/Linked.as @@ -0,0 +1,323 @@ +import empire_ai.weasel.WeaselAI; +import empire_ai.weasel.race.Race; + +import empire_ai.weasel.Movement; +import empire_ai.weasel.Military; +import empire_ai.weasel.Construction; +import empire_ai.weasel.Designs; +import empire_ai.weasel.Development; +import empire_ai.weasel.Systems; +import empire_ai.weasel.Budget; + +from orbitals import getOrbitalModuleID; + +const double MAINFRAME_MIN_DISTANCE_STAGE = 15000; +const double MAINFRAME_MIN_DISTANCE_DEVELOP = 20000; +const double MAINFRAME_MIN_TIMER = 3.0 * 60.0; +const int MAINFRAME_BUILD_MOVE_HOPS = 5; + +class LinkRegion : Savable { + Region@ region; + Object@ obj; + bool arrived = false; + vec3d destination; + + void save(SaveFile& file) { + file << region; + file << obj; + file << arrived; + file << destination; + } + + void load(SaveFile& file) { + file >> region; + file >> obj; + file >> arrived; + file >> destination; + } +}; + +class Linked : Race { + Military@ military; + Designs@ designs; + Construction@ construction; + Development@ development; + Systems@ systems; + Budget@ budget; + + array tracked; + array unassigned; + + BuildOrbital@ buildMainframe; + int mainframeId = -1; + + double nextBuildTry = 15.0 * 60.0; + + void create() override { + @military = cast(ai.military); + @designs = cast(ai.designs); + @construction = cast(ai.construction); + @development = cast(ai.development); + @systems = cast(ai.systems); + @budget = cast(ai.budget); + + mainframeId = getOrbitalModuleID("Mainframe"); + } + + void save(SaveFile& file) override { + construction.saveConstruction(file, buildMainframe); + file << nextBuildTry; + + uint cnt = tracked.length; + file << cnt; + for(uint i = 0; i < cnt; ++i) + file << tracked[i]; + + cnt = unassigned.length; + file << cnt; + for(uint i = 0; i < cnt; ++i) + file << unassigned[i]; + } + + void load(SaveFile& file) override { + @buildMainframe = cast(construction.loadConstruction(file)); + file >> nextBuildTry; + + uint cnt = 0; + file >> cnt; + for(uint i = 0; i < cnt; ++i) { + LinkRegion gt; + file >> gt; + tracked.insertLast(gt); + } + + file >> cnt; + for(uint i = 0; i < cnt; ++i) { + Object@ obj; + file >> obj; + if(obj !is null) + unassigned.insertLast(obj); + } + } + + LinkRegion@ get(Region@ reg) { + for(uint i = 0, cnt = tracked.length; i < cnt; ++i) { + if(tracked[i].region is reg) + return tracked[i]; + } + return null; + } + + void remove(LinkRegion@ gt) { + if(gt.obj !is null && gt.obj.valid && gt.obj.owner is ai.empire) + unassigned.insertLast(gt.obj); + tracked.remove(gt); + } + + Object@ getClosestMainframe(const vec3d& position) { + Object@ closest; + double minDist = INFINITY; + for(uint i = 0, cnt = tracked.length; i < cnt; ++i) { + Object@ obj = tracked[i].obj; + if(obj is null) + continue; + if(!tracked[i].arrived) + continue; + double d = obj.position.distanceTo(position); + if(d < minDist) { + minDist = d; + @closest = obj; + } + } + return closest; + } + + LinkRegion@ getClosestLinkRegion(const vec3d& position) { + LinkRegion@ closest; + double minDist = INFINITY; + for(uint i = 0, cnt = tracked.length; i < cnt; ++i) { + double d = tracked[i].region.position.distanceTo(position); + if(d < minDist) { + minDist = d; + @closest = tracked[i]; + } + } + return closest; + } + + void assignTo(LinkRegion@ gt, Object@ closest) { + unassigned.remove(closest); + @gt.obj = closest; + gt.arrived = false; + + if(closest.region is gt.region) + gt.arrived = true; + if(!gt.arrived) { + gt.destination = military.getStationPosition(gt.region); + closest.addMoveOrder(gt.destination); + } + } + + bool trackingMainframe(Object@ obj) { + for(uint i = 0, cnt = unassigned.length; i < cnt; ++i) { + if(unassigned[i] is obj) + return true; + } + for(uint i = 0, cnt = tracked.length; i < cnt; ++i) { + if(tracked[i].obj is obj) + return true; + } + return false; + } + + bool shouldHaveMainframe(Region@ reg, bool always = false) { + if(military.getBase(reg) !is null) + return true; + if(development.isDevelopingIn(reg)) + return true; + return false; + } + + void focusTick(double time) override { + if(ai.behavior.forbidConstruction) return; + + //Manage unassigned mainframes list + for(uint i = 0, cnt = unassigned.length; i < cnt; ++i) { + Object@ obj = unassigned[i]; + if(obj is null || !obj.valid || obj.owner !is ai.empire) { + unassigned.removeAt(i); + --i; --cnt; + } + } + + //Detect new gates + auto@ data = ai.empire.getOrbitals(); + Object@ obj; + while(receive(data, obj)) { + if(obj is null) + continue; + Orbital@ orb = cast(obj); + if(orb is null || orb.coreModule != uint(mainframeId)) + continue; + if(!trackingMainframe(obj)) + unassigned.insertLast(obj); + } + + //Update existing gates for staging bases + for(uint i = 0, cnt = tracked.length; i < cnt; ++i) { + auto@ gt = tracked[i]; + bool checkAlways = false; + if(gt.obj !is null) { + if(!gt.obj.valid || gt.obj.owner !is ai.empire || (gt.arrived && gt.obj.region !is gt.region)) { + @gt.obj = null; + gt.arrived = false; + checkAlways = true; + } + else if(!gt.arrived && !gt.obj.hasOrders) { + if(gt.destination.distanceTo(gt.obj.position) < 10.0) + gt.arrived = true; + else + gt.obj.addMoveOrder(gt.destination); + } + } + if(!shouldHaveMainframe(gt.region, checkAlways)) { + remove(tracked[i]); + --i; --cnt; + } + } + + //Detect new staging bases to build mainframes at + for(uint i = 0, cnt = military.stagingBases.length; i < cnt; ++i) { + auto@ base = military.stagingBases[i]; + if(base.occupiedTime < MAINFRAME_MIN_TIMER) + continue; + + if(get(base.region) is null) { + LinkRegion@ closest = getClosestLinkRegion(base.region.position); + if(closest !is null && closest.region.position.distanceTo(base.region.position) < MAINFRAME_MIN_DISTANCE_STAGE) + continue; + + LinkRegion gt; + @gt.region = base.region; + tracked.insertLast(gt); + break; + } + } + + //Detect new important planets to build mainframes at + for(uint i = 0, cnt = development.focuses.length; i < cnt; ++i) { + auto@ focus = development.focuses[i]; + Region@ reg = focus.obj.region; + if(reg is null) + continue; + + if(get(reg) is null) { + LinkRegion@ closest = getClosestLinkRegion(reg.position); + if(closest !is null && closest.region.position.distanceTo(reg.position) < MAINFRAME_MIN_DISTANCE_DEVELOP) + continue; + + LinkRegion gt; + @gt.region = reg; + tracked.insertLast(gt); + break; + } + } + + //See if we should build a new mainframe + if(buildMainframe !is null) { + if(buildMainframe.completed) { + @buildMainframe = null; + nextBuildTry = gameTime + 60.0; + } + } + for(uint i = 0, cnt = tracked.length; i < cnt; ++i) { + auto@ gt = tracked[i]; + if(gt.obj is null) { + Object@ closest; + double closestDist = INFINITY; + for(uint n = 0, ncnt = unassigned.length; n < ncnt; ++n) { + Object@ obj = unassigned[n]; + if(obj.region is gt.region) { + @closest = obj; + break; + } + if(!obj.hasMover) + continue; + if(buildMainframe is null && gameTime > nextBuildTry) { + double d = obj.position.distanceTo(gt.region.position); + if(d < closestDist) { + closestDist = d; + @closest = obj; + } + } + } + + if(closest !is null) { + if(log) + ai.print("Assign mainframe to => "+gt.region.name, closest.region); + assignTo(gt, closest); + } else if(buildMainframe is null && gameTime > nextBuildTry) { + if(log) + ai.print("Build mainframe for this system", gt.region); + + bool buildLocal = true; + auto@ factory = construction.primaryFactory; + if(factory !is null) { + Region@ factRegion = factory.obj.region; + if(factRegion !is null && systems.hopDistance(gt.region, factRegion) < MAINFRAME_BUILD_MOVE_HOPS) + buildLocal = false; + } + + if(buildLocal) + @buildMainframe = construction.buildLocalOrbital(getOrbitalModule(mainframeId)); + else + @buildMainframe = construction.buildOrbital(getOrbitalModule(mainframeId), military.getStationPosition(gt.region)); + } + } + } + } +}; + +AIComponent@ createLinked() { + return Linked(); +} ADDED scripts/server/empire_ai/weasel/race/Mechanoid.as Index: scripts/server/empire_ai/weasel/race/Mechanoid.as ================================================================== --- scripts/server/empire_ai/weasel/race/Mechanoid.as +++ scripts/server/empire_ai/weasel/race/Mechanoid.as @@ -0,0 +1,350 @@ +import empire_ai.weasel.WeaselAI; +import empire_ai.weasel.race.Race; + +import empire_ai.weasel.Resources; +import empire_ai.weasel.Colonization; +import empire_ai.weasel.Construction; +import empire_ai.weasel.Movement; +import empire_ai.weasel.Planets; +import empire_ai.weasel.Budget; + +import resources; +import abilities; +import planet_levels; +from constructions import getConstructionType, ConstructionType; +from abilities import getAbilityID; +import oddity_navigation; + +const double MAX_POP_BUILDTIME = 3.0 * 60.0; + +class Mechanoid : Race, RaceResources, RaceColonization { + Colonization@ colonization; + Construction@ construction; + Movement@ movement; + Budget@ budget; + Planets@ planets; + + const ResourceType@ unobtanium; + const ResourceType@ crystals; + int unobtaniumAbl = -1; + + const ResourceClass@ foodClass; + const ResourceClass@ waterClass; + const ResourceClass@ scalableClass; + const ConstructionType@ buildPop; + + int colonizeAbl = -1; + + array popRequests; + array popSources; + array popFactories; + + void create() { + @colonization = cast(ai.colonization); + @construction = cast(ai.construction); + @movement = cast(ai.movement); + @planets = cast(ai.planets); + @budget = cast(ai.budget); + + @ai.defs.Shipyard = null; + + @crystals = getResource("FTL"); + @unobtanium = getResource("Unobtanium"); + unobtaniumAbl = getAbilityID("UnobtaniumMorph"); + + @foodClass = getResourceClass("Food"); + @waterClass = getResourceClass("WaterType"); + @scalableClass = getResourceClass("Scalable"); + + colonizeAbl = getAbilityID("MechanoidColonize"); + colonization.performColonization = false; + + @buildPop = getConstructionType("MechanoidPopulation"); + } + + void start() { + //Oh yes please can we have some ftl crystals sir + if(crystals !is null) { + ResourceSpec spec; + spec.type = RST_Specific; + @spec.resource = crystals; + spec.isLevelRequirement = false; + spec.isForImport = false; + + colonization.queueColonize(spec); + } + } + + void levelRequirements(Object& obj, int targetLevel, array& specs) override { + //Remove all food and water resources + if(obj.levelChain != baseLevelChain.id) + return; + for(int i = specs.length-1; i >= 0; --i) { + auto@ spec = specs[i]; + if(spec.type == RST_Class && (spec.cls is foodClass || spec.cls is waterClass)) + specs.removeAt(i); + } + } + + double transferCost(double dist) { + return 20 + dist * 0.002; + } + + bool orderColonization(ColonizeData& data, Planet@ sourcePlanet) { + return false; + } + + double getGenericUsefulness(const ResourceType@ type) override { + if(type.cls is foodClass || type.cls is waterClass) + return 0.00001; + if(type.level == 1) + return 100.0; + return 1.0; + } + + bool canBuildPopulation(Planet& pl, double factor=1.0) { + if(buildPop is null) + return false; + if(!buildPop.canBuild(pl, ignoreCost=true)) + return false; + auto@ primFact = construction.primaryFactory; + if(primFact !is null && pl is primFact.obj) + return true; + + double laborCost = buildPop.getLaborCost(pl); + double laborIncome = pl.laborIncome; + return laborCost < laborIncome * MAX_POP_BUILDTIME * factor; + } + + bool requiresPopulation(Planet& pl, double mod = 0.0) { + double curPop = pl.population + mod; + double maxPop = pl.maxPopulation; + return curPop < maxPop; + } + + bool canSendPopulation(Planet& pl, double mod = 0.0) { + double curPop = pl.population + mod; + double maxPop = pl.maxPopulation; + if(curPop >= maxPop + 1) + return true; + //auto@ primFact = construction.primaryFactory; + //if(primFact !is null && pl is primFact.obj) { + // uint minFacts = 2; + // if(popFactories.find(pl) == -1) + // minFacts -= 1; + // if(popFactories.length >= minFacts) + // return false; + //} + //if(canBuildPopulation(pl)) { + // if(curPop >= maxPop) + // return true; + //} + return false; + } + + uint chkInd = 0; + array availSources; + void focusTick(double time) override { + if(ai.behavior.forbidColonization) return; + + //Check existing lists + for(uint i = 0, cnt = popFactories.length; i < cnt; ++i) { + auto@ obj = popFactories[i]; + if(obj is null || !obj.valid || obj.owner !is ai.empire) { + popFactories.removeAt(i); + --i; --cnt; + continue; + } + if(!canBuildPopulation(popFactories[i])) { + popFactories.removeAt(i); + --i; --cnt; + continue; + } + } + + for(uint i = 0, cnt = popSources.length; i < cnt; ++i) { + auto@ obj = popSources[i]; + if(obj is null || !obj.valid || obj.owner !is ai.empire) { + popSources.removeAt(i); + --i; --cnt; + continue; + } + if(!canSendPopulation(popSources[i])) { + popSources.removeAt(i); + --i; --cnt; + continue; + } + } + + for(uint i = 0, cnt = popRequests.length; i < cnt; ++i) { + auto@ obj = popRequests[i]; + if(obj is null || !obj.valid || obj.owner !is ai.empire) { + popRequests.removeAt(i); + --i; --cnt; + continue; + } + if(!requiresPopulation(popRequests[i])) { + popRequests.removeAt(i); + --i; --cnt; + continue; + } + } + + //Find new planets to add to our lists + bool checkMorph = false; + Planet@ hw = ai.empire.Homeworld; + if(hw !is null && hw.valid && hw.owner is ai.empire && unobtanium !is null) { + if(hw.primaryResourceType == unobtanium.id) + checkMorph = true; + } + + uint plCnt = planets.planets.length; + for(uint n = 0, cnt = min(15, plCnt); n < cnt; ++n) { + chkInd = (chkInd+1) % plCnt; + auto@ plAI = planets.planets[chkInd]; + + //Find planets that can build population reliably + if(canBuildPopulation(plAI.obj)) { + if(popFactories.find(plAI.obj) == -1) + popFactories.insertLast(plAI.obj); + } + + //Find planets that need population + if(requiresPopulation(plAI.obj)) { + if(popRequests.find(plAI.obj) == -1) + popRequests.insertLast(plAI.obj); + } + + //Find planets that have extra population + if(canSendPopulation(plAI.obj)) { + if(popSources.find(plAI.obj) == -1) + popSources.insertLast(plAI.obj); + } + + if(plAI.resources !is null && plAI.resources.length != 0) { + auto@ res = plAI.resources[0]; + + //Get rid of food and water we don't need + if(res.resource.cls is foodClass || res.resource.cls is waterClass) { + if(res.request is null && !ai.behavior.forbidScuttle) { + Region@ reg = res.obj.region; + if(reg !is null && reg.getPlanetCount(ai.empire) >= 2) { + plAI.obj.abandon(); + } + } + } + + //See if we have anything useful to morph our homeworld too + if(checkMorph) { + bool morph = false; + if(res.resource is crystals) + morph = true; + else if(res.resource.level >= 2 && res.resource.tilePressure[TR_Labor] >= 5) + morph = true; + else if(res.resource.level >= 3 && res.resource.totalPressure > 10) + morph = true; + else if(res.resource.cls is scalableClass && gameTime > 30.0 * 60.0) + morph = true; + else if(res.resource.level >= 2 && res.resource.totalPressure >= 5 && gameTime > 60.0 * 60.0) + morph = true; + + if(morph) { + if(log) + ai.print("Morph homeworld to "+res.resource.name+" from "+res.obj.name, hw); + hw.activateAbilityTypeFor(ai.empire, unobtaniumAbl, plAI.obj); + } + } + } + } + + //See if we can find something to send population to + availSources = popSources; + + for(uint i = 0, cnt = popRequests.length; i < cnt; ++i) { + Planet@ dest = popRequests[i]; + if(canBuildPopulation(dest, factor=(availSources.length == 0 ? 2.5 : 1.5))) { + Factory@ f = construction.get(dest); + if(f !is null) { + if(f.active is null) { + auto@ build = construction.buildConstruction(buildPop); + construction.buildNow(build, f); + if(log) + ai.print("Build population", f.obj); + continue; + } + else { + auto@ cons = cast(f.active); + if(cons !is null && cons.consType is buildPop) { + if(double(dest.maxPopulation) <= dest.population + 0.0) + continue; + } + } + } + } + transferBest(dest, availSources); + } + + if(availSources.length != 0) { + //If we have any population left, do stuff from our colonization queue + for(uint i = 0, cnt = colonization.awaitingSource.length; i < cnt && availSources.length != 0; ++i) { + Planet@ dest = colonization.awaitingSource[i].target; + Planet@ source = transferBest(dest, availSources); + if(source !is null) { + @colonization.awaitingSource[i].colonizeFrom = source; + colonization.awaitingSource.removeAt(i); + --i; --cnt; + } + } + } + + //Build population on idle planets + if(budget.canSpend(BT_Development, 100)) { + for(int i = popFactories.length-1; i >= 0; --i) { + Planet@ dest = popFactories[i]; + Factory@ f = construction.get(dest); + if(f is null || f.active !is null) + continue; + if(dest.population >= double(dest.maxPopulation) + 1.0) + continue; + + auto@ build = construction.buildConstruction(buildPop); + construction.buildNow(build, f); + if(log) + ai.print("Build population for idle", f.obj); + break; + } + } + } + + Planet@ transferBest(Planet& dest, array& availSources) { + //Find closest source + Planet@ bestSource; + double bestDist = INFINITY; + for(uint j = 0, jcnt = availSources.length; j < jcnt; ++j) { + double d = movement.getPathDistance(availSources[j].position, dest.position); + if(d < bestDist) { + bestDist = d; + @bestSource = availSources[j]; + } + } + + if(bestSource !is null) { + double cost = transferCost(bestDist); + if(cost <= ai.empire.FTLStored) { + if(log) + ai.print("Transfering population to "+dest.name, bestSource); + availSources.remove(bestSource); + bestSource.activateAbilityTypeFor(ai.empire, colonizeAbl, dest); + return bestSource; + } + } + return null; + } + + void tick(double time) override { + } +}; + +AIComponent@ createMechanoid() { + return Mechanoid(); +} ADDED scripts/server/empire_ai/weasel/race/Race.as Index: scripts/server/empire_ai/weasel/race/Race.as ================================================================== --- scripts/server/empire_ai/weasel/race/Race.as +++ scripts/server/empire_ai/weasel/race/Race.as @@ -0,0 +1,4 @@ +import empire_ai.weasel.WeaselAI; + +class Race : AIComponent { +}; ADDED scripts/server/empire_ai/weasel/race/StarChildren.as Index: scripts/server/empire_ai/weasel/race/StarChildren.as ================================================================== --- scripts/server/empire_ai/weasel/race/StarChildren.as +++ scripts/server/empire_ai/weasel/race/StarChildren.as @@ -0,0 +1,540 @@ +import empire_ai.weasel.WeaselAI; +import empire_ai.weasel.race.Race; + +import empire_ai.weasel.Colonization; +import empire_ai.weasel.Resources; +import empire_ai.weasel.Construction; +import empire_ai.weasel.Development; +import empire_ai.weasel.Fleets; +import empire_ai.weasel.Movement; +import empire_ai.weasel.Planets; +import empire_ai.weasel.Designs; + +import oddity_navigation; +from abilities import getAbilityID; +from statuses import getStatusID; + +class HabitatMission : Mission { + Planet@ target; + MoveOrder@ move; + double timer = 0.0; + + void save(Fleets& fleets, SaveFile& file) override { + file << target; + file << timer; + fleets.movement.saveMoveOrder(file, move); + } + + void load(Fleets& fleets, SaveFile& file) override { + file >> target; + file >> timer; + @move = fleets.movement.loadMoveOrder(file); + } + + void start(AI& ai, FleetAI& fleet) override { + uint prior = MP_Normal; + if(gameTime < 30.0 * 60.0) + prior = MP_Critical; + @move = cast(ai.movement).move(fleet.obj, target, prior); + } + + void tick(AI& ai, FleetAI& fleet, double time) override { + if(move !is null) { + if(move.failed) { + canceled = true; + return; + } + if(move.completed) { + int ablId = cast(ai.race).habitatAbl; + fleet.obj.activateAbilityTypeFor(ai.empire, ablId, target); + + @move = null; + timer = gameTime + 60.0; + } + } + else { + if(target is null || !target.valid || target.quarantined + || (target.owner !is ai.empire && target.owner.valid) + || target.inCombat) { + canceled = true; + return; + } + + double maxPop = max(double(target.maxPopulation), double(getPlanetLevel(target, target.primaryResourceLevel).population)); + double curPop = target.population; + if(curPop >= maxPop) { + completed = true; + return; + } + + if(gameTime >= timer) { + int popStatus = cast(ai.race).popStatus; + if(target.getStatusStackCountAny(popStatus) >= 5) { + canceled = true; + return; + } + } + } + } +}; + +class LaborMission : Mission { + Planet@ target; + MoveOrder@ move; + double timer = 0.0; + + void save(Fleets& fleets, SaveFile& file) override { + file << target; + file << timer; + fleets.movement.saveMoveOrder(file, move); + } + + void load(Fleets& fleets, SaveFile& file) override { + file >> target; + file >> timer; + @move = fleets.movement.loadMoveOrder(file); + } + + void start(AI& ai, FleetAI& fleet) override { + @move = cast(ai.movement).move(fleet.obj, target); + } + + void tick(AI& ai, FleetAI& fleet, double time) override { + if(move !is null) { + if(move.failed) { + canceled = true; + return; + } + if(move.completed) { + @move = null; + timer = gameTime + 10.0; + } + } + else { + if(target is null || !target.valid || target.quarantined + || target.owner !is ai.empire) { + canceled = true; + return; + } + + if(gameTime >= timer) { + int popStatus = cast(ai.race).popStatus; + timer = gameTime + 10.0; + if(target.getStatusStackCountAny(popStatus) >= 10) { + completed = true; + return; + } + } + } + } +}; + +class StarChildren : Race { + Colonization@ colonization; + Construction@ construction; + Development@ development; + Movement@ movement; + Planets@ planets; + Fleets@ fleets; + Designs@ designs; + + DesignTarget@ mothershipDesign; + double idleSince = 0; + + array motherships; + + int habitatAbl = -1; + int popStatus = -1; + + array popRequests; + array laborPlanets; + + BuildFlagship@ mcBuild; + BuildOrbital@ yardBuild; + + void save(SaveFile& file) override { + designs.saveDesign(file, mothershipDesign); + file << idleSince; + construction.saveConstruction(file, mcBuild); + construction.saveConstruction(file, yardBuild); + + uint cnt = motherships.length; + file << cnt; + for(uint i = 0; i < cnt; ++i) + fleets.saveAI(file, motherships[i]); + } + + void load(SaveFile& file) override { + @mothershipDesign = designs.loadDesign(file); + file >> idleSince; + @mcBuild = cast(construction.loadConstruction(file)); + @yardBuild = cast(construction.loadConstruction(file)); + + uint cnt = 0; + file >> cnt; + for(uint i = 0; i < cnt; ++i) { + auto@ flAI = fleets.loadAI(file); + if(flAI !is null) + motherships.insertLast(flAI); + } + } + + void create() override { + @colonization = cast(ai.colonization); + colonization.performColonization = false; + + @development = cast(ai.development); + development.managePlanetPressure = false; + development.buildBuildings = false; + + @fleets = cast(ai.fleets); + @construction = cast(ai.construction); + @planets = cast(ai.planets); + @designs = cast(ai.designs); + @movement = cast(ai.movement); + + @ai.defs.Factory = null; + @ai.defs.LaborStorage = null; + + habitatAbl = getAbilityID("MothershipColonize"); + popStatus = getStatusID("MothershipPopulation"); + } + + void start() override { + //Get the Tier 1 in our home system + { + ResourceSpec spec; + spec.type = RST_Level_Specific; + spec.level = 1; + spec.isForImport = false; + spec.isLevelRequirement = false; + + colonization.queueColonize(spec); + } + + //Then find a Tier 2 to get + { + ResourceSpec spec; + spec.type = RST_Level_Specific; + spec.level = 2; + spec.isForImport = false; + spec.isLevelRequirement = false; + + colonization.queueColonize(spec); + } + + //Design a mothership + @mothershipDesign = designs.design(DP_Mothership, 500); + mothershipDesign.targetMaintenance = 300; + mothershipDesign.targetLaborCost = 110; + mothershipDesign.customName = "Mothership"; + } + + bool requiresPopulation(Planet& target) { + double maxPop = max(double(target.maxPopulation), double(getPlanetLevel(target, target.primaryResourceLevel).population)); + double curPop = target.population; + return curPop < maxPop; + } + + uint chkInd = 0; + void focusTick(double time) override { + if(ai.behavior.forbidColonization) return; + + //Detect motherships + for(uint i = 0, cnt = fleets.fleets.length; i < cnt; ++i) { + auto@ flAI = fleets.fleets[i]; + if(flAI.fleetClass != FC_Mothership) + continue; + + if(motherships.find(flAI) == -1) { + //Add to our tracking list + flAI.obj.autoFillSupports = false; + flAI.obj.allowFillFrom = true; + motherships.insertLast(flAI); + + //Add as a factory + construction.registerFactory(flAI.obj); + } + } + + for(uint i = 0, cnt = motherships.length; i < cnt; ++i) { + Object@ obj = motherships[i].obj; + if(obj is null || !obj.valid || obj.owner !is ai.empire) { + motherships.removeAt(i); + --i; --cnt; + } + } + + //Detect planets that require more population + for(uint i = 0, cnt = popRequests.length; i < cnt; ++i) { + auto@ obj = popRequests[i]; + if(obj is null || !obj.valid || obj.owner !is ai.empire) { + popRequests.removeAt(i); + --i; --cnt; + continue; + } + if(!requiresPopulation(obj)) { + popRequests.removeAt(i); + --i; --cnt; + continue; + } + } + + for(uint i = 0, cnt = laborPlanets.length; i < cnt; ++i) { + auto@ obj = laborPlanets[i]; + if(obj is null || !obj.valid || obj.owner !is ai.empire) { + laborPlanets.removeAt(i); + --i; --cnt; + continue; + } + if(obj.laborIncome < 3.0/60.0) { + laborPlanets.removeAt(i); + --i; --cnt; + continue; + } + } + + uint plCnt = planets.planets.length; + for(uint n = 0, cnt = min(15, plCnt); n < cnt; ++n) { + chkInd = (chkInd+1) % plCnt; + auto@ plAI = planets.planets[chkInd]; + + //Find planets that need population + if(requiresPopulation(plAI.obj)) { + if(popRequests.find(plAI.obj) == -1) + popRequests.insertLast(plAI.obj); + } + + //Find planets that have labor + if(plAI.obj.laborIncome >= 3.0/60.0) { + if(laborPlanets.find(plAI.obj) == -1) + laborPlanets.insertLast(plAI.obj); + } + } + + //Send motherships to do colonization + uint totalCount = popRequests.length + colonization.awaitingSource.length; + uint motherCount = idleMothershipCount(); + + /*if(motherCount > totalCount) {*/ + for(uint i = 0, cnt = popRequests.length; i < cnt; ++i) { + Planet@ dest = popRequests[i]; + if(isColonizing(dest)) + continue; + if(dest.inCombat) + continue; + + colonizeBest(dest); + } + + for(uint i = 0, cnt = colonization.awaitingSource.length; i < cnt; ++i) { + Planet@ dest = colonization.awaitingSource[i].target; + if(isColonizing(dest)) + continue; + + colonizeBest(dest); + } + /*}*/ + /*else {*/ + /* for(uint i = 0, cnt = motherships.length; i < cnt; ++i) {*/ + /* auto@ flAI = motherships[i];*/ + /* if(flAI.mission !is null)*/ + /* continue;*/ + /* if(isBuildingWithLabor(flAI))*/ + /* continue;*/ + + /* colonizeBest(flAI);*/ + /* }*/ + /*}*/ + + if(totalCount != 0) + idleSince = gameTime; + + //See if we should build new motherships + uint haveMC = motherships.length; + uint wantMC = 1; + if(gameTime > 20.0 * 60.0) + wantMC += 1; + wantMC = max(wantMC, min(laborPlanets.length, uint(gameTime/(30.0*60.0)))); + + if(mcBuild !is null && mcBuild.completed) + @mcBuild = null; + if(wantMC > haveMC && mcBuild is null) + @mcBuild = construction.buildFlagship(mothershipDesign, force=true); + + if(yardBuild is null && haveMC > 0 && gameTime > 60 && gameTime < 180 && ai.defs.Shipyard !is null) { + Region@ reg = motherships[0].obj.region; + if(reg !is null) { + vec3d pos = reg.position; + vec2d offset = random2d(reg.radius * 0.4, reg.radius * 0.8); + pos.x += offset.x; + pos.z += offset.y; + + @yardBuild = construction.buildOrbital(ai.defs.Shipyard, pos); + } + } + + if(motherships.length == 1) + @colonization.colonizeWeightObj = motherships[0].obj; + else + @colonization.colonizeWeightObj = null; + + //Idle motherships should be sent to go collect labor from labor planets + if(laborPlanets.length != 0) { + for(uint i = 0, cnt = motherships.length; i < cnt; ++i) { + auto@ flAI = motherships[i]; + if(flAI.mission !is null) + continue; + if(isAtLaborPlanet(flAI)) + continue; + if(i == 0 && idleSince < gameTime-60.0) + continue; + + double bestDist = INFINITY; + Planet@ best; + for(uint n = 0, ncnt = laborPlanets.length; n < ncnt; ++n) { + Planet@ check = laborPlanets[n]; + if(hasMothershipAt(check)) + continue; + + double d = movement.getPathDistance(flAI.obj.position, check.position); + if(d < bestDist) { + @best = check; + bestDist = d; + } + } + + if(best !is null) { + LaborMission miss; + @miss.target = best; + + fleets.performMission(flAI, miss); + } + } + } + } + + bool isAtLaborPlanet(FleetAI& flAI) { + auto@ miss = cast(flAI); + if(miss !is null) + return true; + + for(uint i = 0, cnt = laborPlanets.length; i < cnt; ++i) { + if(flAI.obj.isLockedOrbit(laborPlanets[i])) + return true; + } + return false; + } + + bool isBuildingWithLabor(FleetAI& flAI) { + auto@ f = construction.get(flAI.obj); + if(f !is null && f.active !is null) + return false; + if(isAtLaborPlanet(flAI)) + return true; + return false; + } + + bool hasMothershipAt(Planet& pl) { + for(uint i = 0, cnt = motherships.length; i < cnt; ++i) { + auto@ flAI = motherships[i]; + + auto@ miss = cast(flAI); + if(miss !is null && miss.target is pl) + return true; + + if(flAI.obj.isLockedOrbit(pl)) + return true; + } + return false; + } + + uint idleMothershipCount() { + uint count = 0; + for(uint i = 0, cnt = motherships.length; i < cnt; ++i) { + if(motherships[i].mission is null) + count += 1; + } + return count; + } + + bool isColonizing(Planet& dest) { + for(uint i = 0, cnt = motherships.length; i < cnt; ++i) { + auto@ flAI = motherships[i]; + auto@ miss = cast(flAI.mission); + if(miss !is null && miss.target is dest) + return true; + } + return false; + } + + Planet@ colonizeBest(FleetAI& flAI) { + Planet@ best; + double bestDist = INFINITY; + for(uint i = 0, cnt = popRequests.length; i < cnt; ++i) { + Planet@ dest = popRequests[i]; + if(isColonizing(dest)) + continue; + if(dest.inCombat) + continue; + + double d = movement.getPathDistance(flAI.obj.position, dest.position); + if(d < bestDist) { + @best = dest; + bestDist = d; + } + } + + if(best is null) { + for(uint i = 0, cnt = colonization.awaitingSource.length; i < cnt; ++i) { + Planet@ dest = colonization.awaitingSource[i].target; + if(isColonizing(dest)) + continue; + + double d = movement.getPathDistance(flAI.obj.position, dest.position); + if(d < bestDist) { + @best = dest; + bestDist = d; + } + } + } + + if(best !is null) { + HabitatMission miss; + @miss.target = best; + + fleets.performMission(flAI, miss); + } + return best; + } + + FleetAI@ colonizeBest(Planet& dest) { + FleetAI@ best; + double bestDist = INFINITY; + for(uint i = 0, cnt = motherships.length; i < cnt; ++i) { + auto@ flAI = motherships[i]; + if(flAI.mission !is null) + continue; + if(isBuildingWithLabor(flAI)) + continue; + + double d = movement.getPathDistance(flAI.obj.position, dest.position); + if(d < bestDist) { + @best = flAI; + bestDist = d; + } + } + + if(best !is null) { + HabitatMission miss; + @miss.target = dest; + + fleets.performMission(best, miss); + } + return best; + } +}; + +AIComponent@ createStarChildren() { + return StarChildren(); +} ADDED scripts/server/empire_ai/weasel/race/Verdant.as Index: scripts/server/empire_ai/weasel/race/Verdant.as ================================================================== --- scripts/server/empire_ai/weasel/race/Verdant.as +++ scripts/server/empire_ai/weasel/race/Verdant.as @@ -0,0 +1,157 @@ +import empire_ai.weasel.WeaselAI; +import empire_ai.weasel.race.Race; + +import empire_ai.weasel.Designs; +import empire_ai.weasel.Development; +import empire_ai.weasel.Planets; + +import buildings; + +class Verdant : Race, RaceDesigns { + Designs@ designs; + Development@ development; + Planets@ planets; + + array defaultDesigns; + array defaultGoals; + + const SubsystemDef@ sinewSubsystem; + const SubsystemDef@ supportSinewSubsystem; + + const BuildingType@ stalk; + + void create() override { + @designs = cast(ai.designs); + @development = cast(ai.development); + @planets = cast(ai.planets); + + @sinewSubsystem = getSubsystemDef("VerdantSinew"); + @supportSinewSubsystem = getSubsystemDef("VerdantSupportSinew"); + @stalk = getBuildingType("Stalk"); + } + + void start() override { + ReadLock lock(ai.empire.designMutex); + for(uint i = 0, cnt = ai.empire.designCount; i < cnt; ++i) { + const Design@ dsg = ai.empire.getDesign(i); + if(dsg.newer !is null) + continue; + if(dsg.updated !is null) + continue; + + uint goal = designs.classify(dsg, DP_Unknown); + if(goal == DP_Unknown) + continue; + + defaultDesigns.insertLast(dsg); + defaultGoals.insertLast(goal); + } + } + + void save(SaveFile& file) override { + uint cnt = defaultDesigns.length; + file << cnt; + for(uint i = 0; i < cnt; ++i) { + file << defaultDesigns[i]; + file << defaultGoals[i]; + } + } + + void load(SaveFile& file) override { + uint cnt = 0; + file >> cnt; + defaultDesigns.length = cnt; + defaultGoals.length = cnt; + for(uint i = 0; i < cnt; ++i) { + file >> defaultDesigns[i]; + file >> defaultGoals[i]; + } + } + + uint plCheck = 0; + void focusTick(double time) override { + if(ai.behavior.forbidConstruction) return; + + //Check if we need to build stalks anywhere + for(uint i = 0, cnt = development.focuses.length; i < cnt; ++i) + checkForStalk(development.focuses[i].plAI); + + uint plCnt = planets.planets.length; + if(plCnt != 0) { + for(uint n = 0; n < min(plCnt, 5); ++n) { + plCheck = (plCheck+1) % plCnt; + auto@ plAI = planets.planets[plCheck]; + checkForStalk(plAI); + } + } + } + + void checkForStalk(PlanetAI@ plAI) { + if(plAI is null) + return; + Planet@ pl = plAI.obj; + if(pl.pressureCap <= 0 && pl.totalPressure >= 1) { + if(planets.isBuilding(plAI.obj, stalk)) + return; + if(pl.getBuildingCount(stalk.id) != 0) + return; + + planets.requestBuilding(plAI, stalk, expire=180.0); + } + } + + bool preCompose(DesignTarget@ target) override { + return false; + } + + bool postCompose(DesignTarget@ target) override { + // auto@ d = target.designer; + + // //Add an extra engine + // if(target.purpose == DP_Combat) + // d.composition.insertAt(0, Exhaust(tag("Engine") & tag("GivesThrust"), 0.25, 0.35)); + + // //Remove armor layers we don't need + // for(uint i = 0, cnt = d.composition.length; i < cnt; ++i) { + // if(cast(d.composition[i]) !is null) { + // d.composition.removeAt(i); + // --i; --cnt; + // } + // } + + return false; + } + + bool design(DesignTarget@ target, int size, const Design@& output) { + //All designs are rescales of default designs + const Design@ baseDesign; + uint possible = 0; + for(uint i = 0, cnt = defaultDesigns.length; i < cnt; ++i) { + if(defaultGoals[i] == target.purpose) { + possible += 1; + if(randomd() < 1.0 / double(possible)) + @baseDesign = defaultDesigns[i]; + } + } + + if(baseDesign is null) + return false; + + //if(target.designer !is null) { + // @target.designer.baseOffDesign = baseDesign; + // if(target.purpose != DP_Support) + // @target.designer.baseOffSubsystem = sinewSubsystem; + // else + // @target.designer.baseOffSubsystem = supportSinewSubsystem; + // @output = target.designer.design(); + //} + + if(output is null) + @output = scaleDesign(baseDesign, size); + return true; + } +}; + +AIComponent@ createVerdant() { + return Verdant(); +} ADDED scripts/server/empire_ai/weasel/searches.as Index: scripts/server/empire_ai/weasel/searches.as ================================================================== --- scripts/server/empire_ai/weasel/searches.as +++ scripts/server/empire_ai/weasel/searches.as @@ -0,0 +1,228 @@ +Object@ findEnemy(Region@ region, Empire@ emp, uint empireMask, bool fleets = true, bool stations = true, bool planets = false) { + array@ objs = findInBox(region.position - vec3d(region.radius), region.position + vec3d(region.radius), empireMask); + uint offset = randomi(0, objs.length-1); + uint cnt = objs.length; + for(uint i = 0; i < cnt; ++i) { + Object@ obj = objs[(i+offset)%cnt]; + Empire@ owner = obj.owner; + + if(!obj.valid) { + continue; + } + else if(owner is null || owner.mask & empireMask == 0) { + continue; + } + else if(emp !is null && !obj.isVisibleTo(emp)) { + continue; + } + else if(obj.region !is region) { + continue; + } + else { + uint type = obj.type; + switch(type) { + case OT_Ship: + if(!obj.hasLeaderAI) + continue; + if(cast(obj).isStation) { + if(!stations) + continue; + } + else { + if(!fleets) + continue; + } + if(obj.getFleetStrength() < 100.0) + continue; + break; + case OT_Orbital: + if(!stations) + continue; + break; + case OT_Planet: + if(!planets) + continue; + break; + default: + continue; + } + } + + return obj; + } + return null; +} + +array@ findEnemies(Region@ region, Empire@ emp, uint empireMask, bool fleets = true, bool stations = true, bool planets = false) { + array@ objs = findInBox(region.position - vec3d(region.radius), region.position + vec3d(region.radius), empireMask); + array outObjs; + for(int i = objs.length-1; i >= 0; --i) { + Object@ obj = objs[i]; + Empire@ owner = obj.owner; + + bool remove = false; + if(!obj.valid) { + remove = true; + } + else if(owner is null || owner.mask & empireMask == 0) { + remove = true; + } + else if(emp !is null && !obj.isVisibleTo(emp)) { + remove = true; + } + else if(obj.region !is region) { + remove = true; + } + else { + uint type = obj.type; + switch(type) { + case OT_Ship: + if(!obj.hasLeaderAI) + remove = true; + if(cast(obj).isStation) { + if(!stations) + remove = true; + } + else { + if(!fleets) + remove = true; + } + if(obj.getFleetStrength() < 100.0) + remove = true; + break; + case OT_Orbital: + if(!stations) + remove = true; + break; + case OT_Planet: + if(!planets) + remove = true; + break; + default: + remove = true; + } + } + + if(!remove) + outObjs.insertLast(obj); + } + return outObjs; +} + +array@ findType(Region@ region, Empire@ emp, uint objectType, uint empireMask = ~0) { + // Specialized for safe object buckets + array@ objs; + DataList@ data; + switch(objectType) + { + case OT_Planet: + @data = region.getPlanets(); + break; + case OT_Pickup: + @data = region.getPickups(); + break; + case OT_Anomaly: + @data = region.getAnomalies(); + break; + case OT_Artifact: + @data = region.getArtifacts(); + break; + case OT_Asteroid: + @data = region.getAsteroids(); + break; + } + + if(data !is null) + { + @objs = array(); + Object@ obj; + while(receive(data, obj)) { + if(obj !is null) + objs.insertLast(obj); + } + } + else { + // No object bucket retrieval mechanism, do a full physics search + @objs = findInBox(region.position - vec3d(region.radius), region.position + vec3d(region.radius), empireMask); + } + + // Generic search using physics system + array outObjs; + for(int i = objs.length-1; i >= 0; --i) { + Object@ obj = objs[i]; + Empire@ owner = obj.owner; + + bool remove = false; + if(!obj.valid) { + remove = true; + } + else if(owner is null || owner.mask & empireMask == 0) { + remove = true; + } + else if(emp !is null && !obj.isVisibleTo(emp)) { + remove = true; + } + else if(obj.region !is region) { + remove = true; + } + else { + uint type = obj.type; + if(type != objectType) + remove = true; + } + + if(!remove) + outObjs.insertLast(obj); + } + return outObjs; +} + +array@ findAll(Region@ region, uint empireMask = ~0) { + return findInBox(region.position - vec3d(region.radius), region.position + vec3d(region.radius), empireMask); +} + +double getTotalFleetStrength(Region@ region, uint empireMask, bool fleets = true, bool stations = true, bool planets = true) { + auto@ objs = findAll(region, empireMask); + double str = 0.0; + for(uint i = 0, cnt = objs.length; i < cnt; ++i) { + Object@ obj = objs[i]; + Empire@ owner = obj.owner; + if(!obj.valid) + continue; + if(owner is null || owner.mask & empireMask == 0) + continue; + if(obj.region !is region) + continue; + + uint type = obj.type; + switch(type) { + case OT_Ship: + if(!obj.hasLeaderAI) + continue; + if(cast(obj).isStation) { + if(!stations) + continue; + } + else { + if(!fleets) + continue; + } + if(obj.getFleetStrength() < 100.0) + continue; + break; + case OT_Orbital: + if(!stations) + continue; + break; + case OT_Planet: + if(!planets) + continue; + break; + default: + continue; + } + + str += sqrt(obj.getFleetStrength()); + } + return str * str; +} ADDED scripts/server/game_start.as Index: scripts/server/game_start.as ================================================================== --- scripts/server/game_start.as +++ scripts/server/game_start.as @@ -0,0 +1,797 @@ +#priority init 1000 +#priority sync 10 +import empire_ai.EmpireAI; +import settings.map_lib; +import settings.game_settings; +import maps; +import map_systems; +import regions.regions; +import artifacts; +from map_generation import generatedSystems, generatedGalaxyGas, GasData; +from empire import Creeps, majorEmpireCount, initEmpireDesigns, sendChatMessage; + +import void createWormhole(SystemDesc@ from, SystemDesc@ to) from "objects.Oddity"; +import Artifact@ makeArtifact(SystemDesc@ system, uint type = uint(-1)) from "map_effects"; + +//Galaxy positioning +Map@[] galaxies; + +vec3d mapLeft; +vec3d mapRight; +double galaxyRadius = 0; + +const double GALAXY_MIN_SPACING = 60000.0; +const double GALAXY_MAX_SPACING = 120000.0; +const double GALAXY_HEIGHT_MARGIN = 50000.0; + +bool overlaps(Map@ from, vec3d point, Map@ to) { + return point.distanceTo(from.origin) < GALAXY_MIN_SPACING + from.radius + to.radius; +} + +//Homeworld searches +class HomeworldSearch { + ScriptThread@ thread; + vec3d goal; + SystemData@ result; + Map@ map; + Empire@ emp; +}; + +double findHomeworld(double time, ScriptThread& thread) { + HomeworldSearch@ search; + thread.getObject(@search); + + @search.result = search.map.findHomeworld(search.emp, search.goal); + thread.stop(); + return 0; +} + +class QualityCalculation { + array galaxies; + array@ homeworlds; +} + +void calculateQuality(QualityCalculation@ data) { + uint homeworldCount = data.homeworlds.length; + array dists(homeworldCount); + + for(uint g = 0, gcnt = data.galaxies.length; g < gcnt; ++g) { + Map@ mp = data.galaxies[g]; + mp.calculateHomeworldDistances(); + + for(uint i = 0, end = mp.systemData.length; i < end; ++i) { + SystemData@ system = mp.systemData[i]; + mp.calculateQuality(system, data.homeworlds, dists); + } + } +} + +void init() { + soundScale = 500.f; + if(isLoadedSave) + return; + + double start = getExactTime(), end = start; + uint hwGalaxies = 0; + + //Create galaxy map instances + for(uint i = 0, cnt = gameSettings.galaxies.length; i < cnt; ++i) { + Map@ desc = getMap(gameSettings.galaxies[i].map_id); + + if(desc !is null) { + for(uint n = 0; n < gameSettings.galaxies[i].galaxyCount; ++n) { + Map@ mp = cast(desc.create()); + @mp.settings = gameSettings.galaxies[i]; + mp.allowHomeworlds = gameSettings.galaxies[i].allowHomeworlds; + if(mp.allowHomeworlds) + hwGalaxies += 1; + + galaxies.insertLast(mp); + } + } + else { + error("Error: Could not find map "+gameSettings.galaxies[i].map_id); + } + } + + if(galaxies.length == 0) { + auto@ _map = cast(getMap("Spiral.SpiralMap").create()); + @_map.settings = MapSettings(); + galaxies.insertLast(_map); + } + + if(hwGalaxies == 0) { + hwGalaxies += 1; + galaxies[0].allowHomeworlds = true; + } + + //Place all the systems + for(uint i = 0, cnt = galaxies.length; i < cnt; ++i) { + galaxies[i].preInit(); + if(galaxies[i].allowHomeworlds) + galaxies[i].estPlayerCount = ceil(double(majorEmpireCount) / double(hwGalaxies)); + else + galaxies[i].estPlayerCount = 0; + galaxies[i].universePlayerCount = majorEmpireCount; + galaxies[i].preGenerate(); + } + + //Place the galaxies + for(uint i = 0, cnt = galaxies.length; i < cnt; ++i) { + vec3d origin; + + if(i != 0) { + double startRad = galaxies[0].radius + galaxies[i].radius + GALAXY_MIN_SPACING; + double endRad = startRad - GALAXY_MIN_SPACING + GALAXY_MAX_SPACING; + + bool overlap = false; + do { + vec2d pos = random2d(startRad, endRad); + + origin = vec3d(pos.x, randomd(-GALAXY_HEIGHT_MARGIN, GALAXY_HEIGHT_MARGIN), pos.y); + overlap = false; + + for(uint j = 0; j < i; ++j) { + if(overlaps(galaxies[j], origin, galaxies[i])) { + overlap = true; + endRad += GALAXY_MIN_SPACING; + break; + } + } + } + while(overlap); + } + + galaxies[i].setOrigin(origin); + galaxyRadius = max(galaxyRadius, origin.length + galaxies[i].radius * 1.4); + } + + //Search for homeworld starting positions in multiple threads (one per empire) + array globalHomeworlds; + { + array sortedEmps; + for(uint i = 0, cnt = getEmpireCount(); i < cnt; ++i) { + Empire@ emp = getEmpire(i); + if(!emp.major) + continue; + sortedEmps.insertLast(TeamSorter(emp)); + } + sortedEmps.sortAsc(); + + array homeworlds(sortedEmps.length); + uint mapCnt = galaxies.length; + uint mapN = randomi(0, mapCnt - 1), mapC = 0; + + for(uint i = 0; i < homeworlds.length; ++i) { + HomeworldSearch@ search = homeworlds[i]; + Empire@ emp = sortedEmps[i].emp; + + //Find a galaxy willing to host this empire + uint j = 0; + do { + @search.map = galaxies[(mapN + mapC) % mapCnt]; + ++mapC; + ++j; + } + while((!search.map.allowHomeworlds || !search.map.canHaveHomeworld(emp)) && j < mapCnt); + + if(mapC >= mapCnt) { + mapN = randomi(0, mapCnt - 1); + mapC = 0; + } + + //Suggested place for this empire + double angle = double(i) * twopi / double(majorEmpireCount); + double rad = search.map.radius * 0.8; + search.goal = vec3d(rad * cos(angle), 0, rad * sin(angle)); + search.goal += search.map.origin; + + //Start the search + @search.emp = emp; + if(search.map.possibleHomeworlds.length == 0) + @search.thread = ScriptThread("game_start::findHomeworld", @search); + else + @search.result = search.map.findHomeworld(search.emp, search.goal); + } + + for(uint i = 0; i < homeworlds.length; ++i) { + HomeworldSearch@ search = homeworlds[i]; + while(search.thread !is null && search.thread.running) sleep(0); + if(search.result !is null) { + search.result.addHomeworld(search.emp); + search.map.markHomeworld(search.result); + } + globalHomeworlds.insertLast(search.result); + } + } + + //Calculate system quality in threads + { + array calcs(6); + for(uint i = 0, cnt = galaxies.length; i < cnt; ++i) + galaxies[i].calculateGalaxyQuality(globalHomeworlds); + + uint n = 0, step = int(ceil(double(galaxies.length) / double(calcs.length))); + for(uint i = 0; i < calcs.length; ++i) { + QualityCalculation@ calc = calcs[i]; + @calc.homeworlds = @globalHomeworlds; + for(uint j = 0; j < step && n < galaxies.length; ++j) { + calc.galaxies.insertLast(galaxies[n]); + n += 1; + } + calculateQuality(calc); + } + } + + //Generate physics + double gridSize = max(modSpacing(7500.0), (galaxyRadius * 2.0) / 150.0); + int gridAmount = (galaxyRadius * 2.0) / gridSize; + setupPhysics(gridSize, gridSize / 8.0, gridAmount); + + //Generate region objects + for(uint i = 0, cnt = galaxies.length; i < cnt; ++i) + galaxies[i].generateRegions(); + for(uint i = 0, cnt = generatedSystems.length; i < cnt; ++i) + generatedSystems[i].object.finalizeCreation(); + + //Actually generate maps + for(uint i = 0, cnt = galaxies.length; i < cnt; ++i) + galaxies[i].generate(); + + //Regenerate the region lookup tree with the actual sizes + regenerateRegionGroups(); + + //Generate wormholes in case of multiple galaxies + if(galaxies.length > 1) { + uint totalSystems = 0; + for(uint i = 0, cnt = galaxies.length; i < cnt; ++i) + totalSystems += galaxies[i].systems.length; + uint wormholes = max(config::GALAXY_MIN_WORMHOLES * galaxies.length, + totalSystems / config::SYSTEMS_PER_WORMHOLE); + if(wormholes % 2 != 0) + wormholes += 1; + uint generated = 0; + + for(uint i = 0, cnt = galaxies.length; i < cnt; ++i) { + auto@ glx = galaxies[i]; + + //Figure out how many wormhole endpoints this galaxy should have + double pct = double(glx.systems.length) / double(totalSystems); + uint amount = max(uint(config::GALAXY_MIN_WORMHOLES), uint(round(pct * wormholes))); + + //Tell the galaxy to distribute them + glx.placeWormholes(amount); + + generated += amount; + } + + //Make a circle of wormhole endpoints + for(uint i = 0, cnt = galaxies.length; i < cnt; ++i) { + auto@ glx = galaxies[i]; + auto@ nextGlx = galaxies[(i+1)%cnt]; + + auto@ from = glx.getWormhole(); + auto@ to = nextGlx.getWormhole(); + if(from is null || to is null) + continue; + + createWormhole(from, to); + glx.addWormhole(from, to); + nextGlx.addWormhole(to, from); + } + + //Randomly spread the remaining wormholes + for(uint i = 0, cnt = galaxies.length; i < cnt; ++i) { + auto@ glx = galaxies[i], otherGlx; + SystemDesc@ hole = glx.getWormhole(); + SystemDesc@ other; + while(hole !is null) { + uint index = randomi(0, cnt - 1); + for(uint n = 0; n < cnt; ++n) { + @otherGlx = galaxies[n]; + @other = otherGlx.getWormhole(); + + if(other !is null) + break; + } + + if(other !is null) { + createWormhole(hole, other); + glx.addWormhole(hole, other); + otherGlx.addWormhole(other, hole); + } + + @hole = glx.getWormhole(); + @other = null; + @otherGlx = null; + } + } + } + + end = getExactTime(); + info("Map generation: "+toString((end - start)*1000,1)+"ms"); + start = end; + + end = getExactTime(); + info("Link generation: "+toString((end - start)*1000,1)+"ms"); + start = end; + + //Deal with generating unique spread artifacts + if(generatedSystems.length > 1 && config::ENABLE_UNIQUE_SPREADS != 0) { + for(uint i = 0, cnt = getArtifactTypeCount(); i < cnt; ++i) { + auto@ type = getArtifactType(i); + if(type.spreadVariable.length == 0) + continue; + if(config::get(type.spreadVariable) <= 0.0) + continue; + + SystemDesc@ sys; + if(type.requireContestation > 0) + @sys = getRandomSystemAboveContestation(type.requireContestation); + if(sys is null) + @sys = getRandomSystem(); + + if(sys !is null) + makeArtifact(sys, type.id); + } + } + + //Initialization for map code + for(uint i = 0, cnt = galaxies.length; i < cnt; ++i) + galaxies[i].initDefs(); + for(uint i = 0, cnt = galaxies.length; i < cnt; ++i) + galaxies[i].init(); + + //Explore entire map if indicated + if(config::START_EXPLORED_MAP != 0.0) { + for(uint i = 0, cnt = systemCount; i < cnt; ++i) + getSystem(i).object.ExploredMask = int(~0); + } + + //Assign already connected players to empires + { + if(playerEmpire !is null && playerEmpire.valid) + CURRENT_PLAYER.linkEmpire(playerEmpire); + uint empInd = 0, empCnt = getEmpireCount(); + array@ players = getPlayers(); + + //First pass: players into player empires + for(uint i = 0, plCnt = players.length; i < plCnt && empInd < empCnt; ++i) { + Player@ pl = players[i]; + connectedPlayers.insertLast(pl); + connectedSet.insert(pl.id); + if(pl.emp is null) { + for(; empInd < empCnt; ++empInd) { + Empire@ emp = getEmpire(empInd); + if(!emp.major) + continue; + if(emp.player !is null) + continue; + //if(emp.getAIType() != ET_Player) + // continue; + + pl.linkEmpire(emp); + ++empInd; + break; + } + } + } + + //Second pass: take over AIs + empInd = 0; + for(uint i = 0, plCnt = players.length; i < plCnt && empInd < empCnt; ++i) { + Player@ pl = players[i]; + if(pl.emp is null) { + for(; empInd < empCnt; ++empInd) { + Empire@ emp = getEmpire(empInd); + if(!emp.major) + continue; + if(emp.player !is null) + continue; + + pl.linkEmpire(emp); + if(pl.name.length != 0) + emp.name = pl.name; + ++empInd; + break; + } + } + } + } +} + +class TeamSorter { + Empire@ emp; + TeamSorter() {} + TeamSorter(Empire@ empire) { + @emp = empire; + } + + int opCmp(const TeamSorter& other) const { + if(emp.team == -1) { + if(other.emp.team == -1) + return 0; + return 1; + } + if(other.emp.team == -1) + return -1; + if(emp.team > other.emp.team) + return 1; + if(emp.team < other.emp.team) + return 1; + return 0; + } +}; + +uint get_systemCount() { + return generatedSystems.length; +} + +SystemDesc@ getSystem(uint index) { + if(index >= generatedSystems.length) + return null; + return generatedSystems[index]; +} + +SystemDesc@ getSystem(Region@ region) { + if(region is null || region.SystemId == -1) + return null; + return generatedSystems[region.SystemId]; +} + +SystemDesc@ getSystem(const string& name) { + //TODO: Use dictionary + uint cnt = systemCount; + for(uint i = 0; i < cnt; ++i) { + if(getSystem(i).name == name) + return getSystem(i); + } + return null; +} + +SystemDesc@ getRandomSystem() { + return generatedSystems[randomi(0, generatedSystems.length-1)]; +} + +SystemDesc@ getRandomSystemAboveContestation(double contest) { + double roll = randomd(); + double total = 0.0; + SystemDesc@ chosen; + for(uint i = 0, cnt = generatedSystems.length; i < cnt; ++i) { + auto@ sys = generatedSystems[i]; + if(sys.contestation < contest) + continue; + + total += 1.0; + double chance = 1.0 / total; + if(roll < chance) { + @chosen = sys; + roll /= chance; + } + else { + roll = (roll - chance) / (1.0 - chance); + } + } + return chosen; +} + +SystemDesc@ getClosestSystem(const vec3d& point) { + SystemDesc@ closest; + double dist = INFINITY; + for(uint i = 0, cnt = generatedSystems.length; i < cnt; ++i) { + double d = generatedSystems[i].position.distanceToSQ(point); + if(d < dist) { + dist = d; + @closest = generatedSystems[i]; + } + } + return closest; +} + +void syncInitial(Message& msg) { + uint cnt = generatedSystems.length; + msg << cnt; + for(uint i = 0; i < cnt; ++i) + generatedSystems[i].write(msg); + + cnt = galaxies.length; + msg << cnt; + for(uint i = 0; i < cnt; ++i) + msg << galaxies[i].id; + + cnt = generatedGalaxyGas.length; + msg << cnt; + for(uint i = 0; i < cnt; ++i) { + GasData@ gas = generatedGalaxyGas[i]; + + msg.writeSmallVec3(gas.position); + msg << float(gas.scale); + + if(gas.gdat.cullingNode !is null) { + msg.write1(); + msg.writeSmallVec3(gas.gdat.cullingNode.position); + msg << float(gas.gdat.cullingNode.scale); + } + else { + msg.write0(); + } + + uint sCnt = gas.sprites.length; + msg.writeSmall(sCnt); + for(uint s = 0; s < sCnt; ++s) { + GasSprite@ sprite = gas.sprites[s]; + msg.writeSmallVec3(sprite.pos); + msg << float(sprite.scale); + msg << sprite.color; + msg.writeBit(sprite.structured); + } + } +} + +bool doSystemSync = false; +bool sendPeriodic(Message& msg) { + if(!doSystemSync) + return false; + + doSystemSync = false; + uint cnt = generatedSystems.length; + msg << cnt; + for(uint i = 0; i < cnt; ++i) + generatedSystems[i].write(msg); + return true; +} + +array connectedPlayers; +set_int connectedSet; +double timer = 0.0; +void tick(double time) { + for(uint i = 0, cnt = galaxies.length; i < cnt; ++i) + galaxies[i].tick(time); + + timer += time; + if(timer >= 1.0) { + timer = 0.0; + array@ players = getPlayers(); + + //Send connect events + for(uint i = 0, cnt = players.length; i < cnt; ++i) { + Player@ pl = players[i]; + string name = pl.name; + if(name.length == 0) + continue; + if(!connectedSet.contains(pl.id)) { + string msg = format("[color=#aaa]"+locale::MP_CONNECT_EVENT+"[/color]", + format("[b]$1[/b]", bbescape(name))); + sendChatMessage(msg, offset=30); + connectedPlayers.insertLast(pl); + connectedSet.insert(pl.id); + } + } + + connectedSet.clear(); + for(uint i = 0, cnt = players.length; i < cnt; ++i) + connectedSet.insert(players[i].id); + + //Send disconnect events + for(uint i = 0, cnt = connectedPlayers.length; i < cnt; ++i) { + if(!connectedSet.contains(connectedPlayers[i].id)) { + Color color; + string name = connectedPlayers[i].name; + Empire@ emp = connectedPlayers[i].emp; + if(emp !is null) + color = emp.color; + + string msg = format("[color=#aaa]"+locale::MP_DISCONNECT_EVENT+"[/color]", + format("[b][color=$1]$2[/color][/b]", toString(color), bbescape(name))); + sendChatMessage(msg, offset=30); + connectedPlayers.removeAt(i); + --i; --cnt; + } + } + } +} + +void getSystems() { + uint cnt = generatedSystems.length; + for(uint i = 0; i < cnt; ++i) + yield(generatedSystems[i]); +} + +void generateNewSystem(const vec3d& pos, double radius, const string& name = "", bool makeLinks = true) { + generateNewSystem(pos, radius, null, name, makeLinks); +} + +void generateNewSystem(const vec3d& pos, double radius, SystemGenerateHook@ hook, const string& name = "", bool makeLinks = true, const string& type = "") { + //Because things access the generated systems list from outside of a locked context for performance, and + //creating new systems is a very very rare thing, we just use an isolation hook here, which pauses the + //execution of the entire game, runs the hook, then resumes. + SystemGenerator sys; + sys.position = pos; + sys.radius = radius; + sys.makeLinks = makeLinks; + sys.makeType = type; + sys.name = name; + @sys.hook = hook; + isolate_run(sys); +} + +interface SystemGenerateHook { + void call(SystemDesc@ desc); +} + +class SystemGenerator : IsolateHook { + vec3d position; + double radius; + string name; + SystemGenerateHook@ hook; + bool makeLinks = true; + string makeType; + + void call() { + if(name.length == 0) { + NameGenerator sysNames; + sysNames.read("data/system_names.txt"); + name = sysNames.generate(); + } + + ObjectDesc sysDesc; + sysDesc.type = OT_Region; + sysDesc.name = name; + sysDesc.flags |= objNoPhysics; + sysDesc.flags |= objNoDamage; + sysDesc.delayedCreation = true; + sysDesc.position = position; + + Region@ region = cast(makeObject(sysDesc)); + region.alwaysVisible = true; + region.InnerRadius = radius / 1.5; + region.OuterRadius = radius; + region.radius = region.OuterRadius; + + SystemData dat; + dat.index = generatedSystems.length; + dat.position = position; + dat.quality = 100; + @dat.systemCode = SystemCode(); + + SystemDesc desc; + desc.index = generatedSystems.length; + region.SystemId = desc.index; + desc.name = region.name; + desc.position = position; + desc.radius = region.OuterRadius; + @desc.object = region; + + generatedSystems.insertLast(desc); + addRegion(desc.object); + + region.finalizeCreation(); + + //Run the type + auto@ sysType = getSystemType(makeType); + if(sysType !is null) { + dat.systemType = sysType.id; + + sysType.generate(dat, desc); + + region.InnerRadius = desc.radius; + region.OuterRadius = desc.radius * 1.5; + region.radius = region.OuterRadius; + desc.radius = region.OuterRadius; + + sysType.postGenerate(dat, desc); + + MapGeneration gen; + gen.finalizeSystem(dat, desc); + } + + //Make trade lines to nearby systems + if(makeLinks) { + SystemDesc@ closest; + array nearby; + double closestDist = INFINITY; + for(uint i = 0, cnt = generatedSystems.length; i < cnt; ++i) { + double d = generatedSystems[i].position.distanceTo(desc.position); + if(generatedSystems[i] is desc) + continue; + if(d < 13000.0) + nearby.insertLast(generatedSystems[i]); + if(d < closestDist) { + closestDist = d; + @closest = generatedSystems[i]; + } + } + + if(nearby.length == 0) { + closest.adjacent.insertLast(desc.index); + closest.adjacentDist.insertLast(closest.position.distanceTo(desc.position)); + desc.adjacent.insertLast(closest.index); + desc.adjacentDist.insertLast(closest.position.distanceTo(desc.position)); + } + else { + for(uint i = 0, cnt = nearby.length; i < cnt; ++i) { + nearby[i].adjacent.insertLast(desc.index); + nearby[i].adjacentDist.insertLast(nearby[i].position.distanceTo(desc.position)); + desc.adjacent.insertLast(nearby[i].index); + desc.adjacentDist.insertLast(nearby[i].position.distanceTo(desc.position)); + } + } + + if(desc.adjacent.length == 0 || config::START_EXPLORED_MAP != 0.0) { + desc.object.ExploredMask.value = int(~0); + } + else { + for(uint i = 0, cnt = desc.adjacent.length; i < cnt; ++i) + desc.object.ExploredMask |= getSystem(desc.adjacent[i]).object.SeenMask; + } + } + + //Create the system node + Node@ snode = bindCullingNode(region, desc.position, 1000.0); + snode.scale = region.radius + 128.0; + snode.rebuildTransform(); + + calcGalaxyExtents(); + + if(hook !is null) + hook.call(desc); + + //Notify clients of changes + refreshClientSystems(CURRENT_PLAYER); + doSystemSync = true; + } +}; + +void save(SaveFile& data) { + data << uint(generatedSystems.length); + for(uint i = 0; i < generatedSystems.length; ++i) + generatedSystems[i].save(data); + data << uint(generatedGalaxies.length); + for(uint i = 0; i < generatedGalaxies.length; ++i) + generatedGalaxies[i].save(data); + data << uint(generatedGalaxyGas.length); + for(uint i = 0; i < generatedGalaxyGas.length; ++i) + generatedGalaxyGas[i].save(data); + data << uint(galaxies.length); + for(uint i = 0; i < galaxies.length; ++i) { + data << galaxies[i].id; + galaxies[i].save(data); + } +} + +void load(SaveFile& data) { + uint count = 0; + data >> count; + generatedSystems.length = count; + for(uint i = 0; i < generatedSystems.length; ++i) { + SystemDesc desc; + desc.load(data); + @generatedSystems[i] = desc; + } + + data >> count; + generatedGalaxies.length = count; + for(uint i = 0; i < count; ++i) { + @generatedGalaxies[i] = GalaxyData(); + generatedGalaxies[i].load(data); + } + + data >> count; + generatedGalaxyGas.length = count; + for(uint i = 0; i < count; ++i) { + @generatedGalaxyGas[i] = GasData(); + generatedGalaxyGas[i].load(data); + } + + if(data >= SV_0040) { + data >> count; + galaxies.length = count; + + for(uint i = 0; i < galaxies.length; ++i) { + string ident; + data >> ident; + @galaxies[i] = getMap(ident).create(); + galaxies[i].load(data); + } + } +}