Index: binaries/data/mods/public/gui/gamesettings/attributes/ResourceBalance.js =================================================================== --- binaries/data/mods/public/gui/gamesettings/attributes/ResourceBalance.js +++ binaries/data/mods/public/gui/gamesettings/attributes/ResourceBalance.js @@ -0,0 +1,64 @@ +GameSettings.prototype.Attributes.ResourceBalance = class ResourceBalance extends GameSetting +{ + init() + { + this.setDataValueHelper(undefined, undefined); + this.settings.map.watch(() => this.onMapChange(), ["map"]); + } + + toInitAttributes(attribs) + { + if (this.value) + attribs.settings.ResourceBalance = this.value; + } + + fromInitAttributes(attribs) + { + if (!this.getLegacySetting(attribs, "ResourceBalance")) + this.setValue(undefined); + else + this.setValue(this.getLegacySetting(attribs, "ResourceBalance")); + } + + onMapChange() + { + let mapData = this.settings.map.data; + if (!mapData || !mapData.settings || !mapData.settings.ResourceBalance) + { + this.setDataValueHelper(undefined, undefined); + return; + } + this.setDataValueHelper(mapData.settings.ResourceBalance, mapData.settings.ResourceBalance[0].Id); + } + + setValue(val) + { + // TODO: more validation. + if (this.data) + this.value = val || "random"; + else + this.value = undefined; + } + + pickRandomItems() + { + // We do not want this setting to be random + if (this.settings.map.map === "random") + this.value = this.settings.map.data.settings.ResourceBalance[0].Id; + + return false; + } + + /** + * Helper function to ensure this.data and this.value + * are assigned in the correct order to prevent + * crashes in the renderer. + * @param {object} data - The day time option data. + * @param {string} value - The option's key. + */ + setDataValueHelper(data, value) + { + this.data = data; + this.value = value; + } +}; Index: binaries/data/mods/public/gui/gamesetup/Pages/GameSetupPage/GameSettings/GameSettingsLayout.js =================================================================== --- binaries/data/mods/public/gui/gamesetup/Pages/GameSetupPage/GameSettings/GameSettingsLayout.js +++ binaries/data/mods/public/gui/gamesetup/Pages/GameSetupPage/GameSettings/GameSettingsLayout.js @@ -16,6 +16,7 @@ "Biome", "SeaLevelRiseTime", "Daytime", + "ResourceBalance", "TriggerDifficulty", "Nomad", "Treasures", Index: binaries/data/mods/public/gui/gamesetup/Pages/GameSetupPage/GameSettings/Single/Dropdowns/ResourceBalance.js =================================================================== --- binaries/data/mods/public/gui/gamesetup/Pages/GameSetupPage/GameSettings/Single/Dropdowns/ResourceBalance.js +++ binaries/data/mods/public/gui/gamesetup/Pages/GameSetupPage/GameSettings/Single/Dropdowns/ResourceBalance.js @@ -0,0 +1,55 @@ +GameSettingControls.ResourceBalance = class ResourceBalance extends GameSettingControlDropdown +{ + constructor(...args) + { + super(...args); + + this.values = undefined; + g_GameSettings.resourceBalance.watch(() => this.render(), ["value", "data"]); + this.render(); + } + + onHoverChange() + { + this.dropdown.tooltip = this.values.Description[this.dropdown.hovered] || this.Tooltip; + } + + render() + { + this.setHidden(!g_GameSettings.resourceBalance.data); + if (!g_GameSettings.resourceBalance.data) + return; + + this.values = prepareForDropdown([ + ...g_GameSettings.resourceBalance.data.map(item => ({ + "Id": item.Id, + "Name": translate(item.Name), + "Description": translate(item.Description) + })) + ]); + + this.dropdown.list = this.values.Name; + this.dropdown.list_data = this.values.Id; + + this.setSelectedValue(g_GameSettings.resourceBalance.value); + } + + getAutocompleteEntries() + { + return this.values && this.values.Name.slice(1); + } + + onSelectionChange(itemIdx) + { + g_GameSettings.resourceBalance.setValue(this.values.Id[itemIdx]); + this.gameSettingsController.setNetworkInitAttributes(); + } +}; + +GameSettingControls.ResourceBalance.prototype.TitleCaption = + translate("Resource balance"); + +GameSettingControls.ResourceBalance.prototype.Tooltip = + translate("Select how resource balance around players should be done"); + +GameSettingControls.ResourceBalance.prototype.AutocompleteOrder = 0; Index: binaries/data/mods/public/maps/random/mainland.js =================================================================== --- binaries/data/mods/public/maps/random/mainland.js +++ binaries/data/mods/public/maps/random/mainland.js @@ -41,6 +41,9 @@ const heightLand = 3; +const resourceBalance = g_MapSettings.ResourceBalance; +const neutralFoodDistance = resourceBalance == "none" ? 20 : 45; + var g_Map = new RandomMap(heightLand, tMainTerrain); const numPlayers = getNumPlayers(); @@ -79,6 +82,20 @@ }, "Decoratives": { "template": aGrassShort + }, + "Balance": resourceBalance, + "NearbyResources": { + "foodConstraints": avoidClasses(clPlayer, 25, clFood, 15), + "berries": { + "template": oFruitBush + }, + "animals": [ + { "template": oMainHuntableAnimal }, + { "template": oSecondaryHuntableAnimal } + ], + "foodAvailability": 1, + "huntBerryRatio": 1, + "foodTileClass": clFood } }); Engine.SetProgress(20); @@ -86,14 +103,14 @@ createBumps(avoidClasses(clPlayer, 20)); if (randBool()) - createHills([tCliff, tCliff, tHill], avoidClasses(clPlayer, 20, clHill, 15), clHill, scaleByMapSize(3, 15)); + createHills([tCliff, tCliff, tHill], avoidClasses(clPlayer, 20, clHill, 15, clFood, 5), clHill, scaleByMapSize(3, 15)); else - createMountains(tCliff, avoidClasses(clPlayer, 20, clHill, 15), clHill, scaleByMapSize(3, 15)); + createMountains(tCliff, avoidClasses(clPlayer, 20, clHill, 15, clFood, 5), clHill, scaleByMapSize(3, 15)); var [forestTrees, stragglerTrees] = getTreeCounts(...rBiomeTreeCount(1)); createDefaultForests( [tMainTerrain, tForestFloor1, tForestFloor2, pForest1, pForest2], - avoidClasses(clPlayer, 20, clForest, 18, clHill, 0), + avoidClasses(clPlayer, 20, clForest, 18, clHill, 0, clFood, 2), clForest, forestTrees); @@ -122,7 +139,7 @@ oMetalSmall, oMetalLarge, clMetal, - avoidClasses(clForest, 1, clPlayer, scaleByMapSize(20, 35), clHill, 1) + avoidClasses(clForest, 1, clPlayer, scaleByMapSize(20, 35), clHill, 1, clFood, 2) ); g_Map.log("Creating stone mines"); @@ -130,7 +147,7 @@ oStoneSmall, oStoneLarge, clRock, - avoidClasses(clForest, 1, clPlayer, scaleByMapSize(20, 35), clHill, 1, clMetal, 10) + avoidClasses(clForest, 1, clPlayer, scaleByMapSize(20, 35), clHill, 1, clMetal, 10, clFood, 2) ); Engine.SetProgress(65); @@ -168,7 +185,7 @@ 3 * numPlayers, 3 * numPlayers ], - avoidClasses(clForest, 0, clPlayer, 20, clHill, 1, clMetal, 4, clRock, 4, clFood, 20), + avoidClasses(clForest, 0, clPlayer, neutralFoodDistance, clHill, 1, clMetal, 4, clRock, 4, clFood, 20), clFood); Engine.SetProgress(75); @@ -180,7 +197,7 @@ [ 3 * numPlayers ], - avoidClasses(clForest, 0, clPlayer, 20, clHill, 1, clMetal, 4, clRock, 4, clFood, 10), + avoidClasses(clForest, 0, clPlayer, neutralFoodDistance, clHill, 1, clMetal, 4, clRock, 4, clFood, 10), clFood); Engine.SetProgress(85); Index: binaries/data/mods/public/maps/random/mainland.json =================================================================== --- binaries/data/mods/public/maps/random/mainland.json +++ binaries/data/mods/public/maps/random/mainland.json @@ -6,6 +6,24 @@ "Preview" : "mainland.png", "Keywords": ["multiplayer"], "SupportedBiomes": "generic/", - "CircularMap" : true + "CircularMap" : true, + "ResourceBalance": + [ + { + "Id": "none", + "Name": "No balance", + "Description": "Resource distribution around player is completely random." + }, + { + "Id": "player", + "Name": "Player balance", + "Description": "Every player will receive the same amount of resources near their base." + }, + { + "Id": "team", + "Name": "Team balance", + "Description": "Every team will receive amount of resources overall. However, the distribution between players may vary." + } + ] } } Index: binaries/data/mods/public/maps/random/rmgen-common/player.js =================================================================== --- binaries/data/mods/public/maps/random/rmgen-common/player.js +++ binaries/data/mods/public/maps/random/rmgen-common/player.js @@ -23,7 +23,8 @@ "Treasures", "Berries", "Chicken", - "Decoratives" + "Decoratives", + "NearbyResources" ]; function isNomad() @@ -139,11 +140,15 @@ g_Map.log("Creating playerbases"); let [playerIDs, playerPosition] = playerBaseArgs.PlayerPlacement; + let playersResourcesCount = getPlayersResourceCount(playerBaseArgs); for (let i = 0; i < getNumPlayers(); ++i) { playerBaseArgs.playerID = playerIDs[i]; playerBaseArgs.playerPosition = playerPosition[i]; + + if (playerBaseArgs.Balance && playerBaseArgs.Balance != "none") + playerBaseArgs.NearbyResources.foodAmount = playersResourcesCount[i]; placePlayerBase(playerBaseArgs); } } @@ -441,6 +446,59 @@ } } +function placePlayerBaseNearbyResources(args) +{ + let [get, basePosition, baseResourceConstraint] = getPlayerBaseArgs(args); + + let area = createArea(new DiskPlacer(get("radius", 30), basePosition), null, null); + let place = function(type, amount) { + let group = new SimpleGroup( + [new SimpleObject(type, amount, amount, 0, 4)], + true, args.foodTileClass + ); + createObjectGroupsByAreas(group, 0, args.foodConstraints, 1, 300, [area]); + }; + let getFoodAmount = function(template) { + return GetBaseTemplateDataValue(Engine.GetTemplate(template), "ResourceSupply/Max"); + }; + let remainingFood = get("foodAmount", 0); + let animals = args.animals; + animals.sort((a, b) => getFoodAmount(a.template) > getFoodAmount(b.template)); + while (remainingFood > 0) + { + if (remainingFood <= 700) + { + // We want to get as close to 0 as possible to end food placement. + // In low quantities of food, berries are less useful, so we generate hunt everytime. + let smallestAnimal = animals[0].template; + let amount = remainingFood / getFoodAmount(smallestAnimal); + place(smallestAnimal, amount); + remainingFood = 0; + } + else + { + if (randBool(0.5 * get("huntBerryRatio", 1))) + { + let currentAnimal = pickRandom(animals).template; + let currentAnimalFood = getFoodAmount(currentAnimal); + let maxAmount = remainingFood / currentAnimalFood; + let desiredAmount = randIntInclusive(5, 7); + desiredAmount = Math.max(desiredAmount, desiredAmount * 100 / currentAnimalFood); + let amount = Math.min(maxAmount, desiredAmount); + remainingFood -= amount * currentAnimalFood; + place(currentAnimal, amount); + } + else + { + let mainBerryFood = getFoodAmount(args.berries.template); + let amount = Math.min(remainingFood, 1200) / mainBerryFood; + remainingFood -= amount * mainBerryFood; + place(args.berries.template, amount); + } + } + } +} + function placePlayersNomad(playerClass, constraints) { if (!isNomad()) @@ -843,3 +901,77 @@ return [playerIDs, startLocations]; } + +function getPlayersResourceCount(playerBaseArgs) +{ + let playerIDs = playerBaseArgs.PlayerPlacement[0]; + if (!playerBaseArgs.NearbyResources) + return playerIDs.map(playerID => 0); + + let huntablesFoodCount = playerBaseArgs.NearbyResources.animals.map( + animal => GetBaseTemplateDataValue(Engine.GetTemplate(animal.template), "ResourceSupply/Max") + ); + let maxHuntableFoodCount = Math.max.apply(Math, huntablesFoodCount); + + if (playerBaseArgs.Balance == "player") + { + let totalFood = randIntInclusive(0, 20) * 100 * playerBaseArgs.NearbyResources.foodAvailability; + totalFood += maxHuntableFoodCount * 5; + if (randBool(0.2)) + totalFood = 0; + + return playerIDs.map(playerID => totalFood); + } + if (playerBaseArgs.Balance == "team") + { + // TODO: rewrite this part using partitionPlayers (I missed it) + let averageFood = 1000 * playerBaseArgs.NearbyResources.foodAvailability + maxHuntableFoodCount * 5; + let sortedPlayers = sortPlayers(playerIDs); + let distribution = []; + let currentTeam = -1; + let currentTeamDistribution = []; + for (let i = 0; i < numPlayers; ++i) + { + let playerTeam = getPlayerTeam(sortedPlayers[i]); + if (playerTeam == -1) + { + distribution[sortedPlayers[i]] = averageFood; + continue; + } + if (playerTeam != currentTeam) + { + currentTeam = playerTeam; + + // Find team size + let teamSize = 1; + for (let j = i + 1; j < numPlayers && getPlayerTeam(sortedPlayers[j]) == currentTeam; ++j, ++teamSize); + + // Assign resources to players in the team + let remainingFood = teamSize * averageFood; + for (let n = teamSize; n > 0; --n) + { + if (n == 1) + { + currentTeamDistribution.push(remainingFood); + continue; + } + let remainingAverage = averageFood; // or remainingFood / n, undecided for now + let deviation = randFloat(-1, 1) * remainingAverage / 2; + let playerFood = remainingAverage + deviation; + + // Round to closest 100 + playerFood = 100 * Math.round(playerFood / 100); + if (playerFood > remainingFood) + playerFood = remainingFood; + if (playerFood <= 700) + playerFood = 0; + remainingFood -= playerFood; + currentTeamDistribution.push(playerFood); + } + } + distribution[sortedPlayers[i]] = currentTeamDistribution.pop(); + } + return playerIDs.map(playerID => distribution[playerID]); + } + return playerIDs.map(playerID => 0); +}