001package com.fs.starfarer.api.impl.campaign.intel.bar.events; 002 003import java.awt.Color; 004import java.util.ArrayList; 005import java.util.LinkedHashSet; 006import java.util.List; 007import java.util.Random; 008import java.util.Set; 009 010import com.fs.starfarer.api.Global; 011import com.fs.starfarer.api.campaign.CargoAPI; 012import com.fs.starfarer.api.campaign.FactionAPI; 013import com.fs.starfarer.api.campaign.OptionPanelAPI; 014import com.fs.starfarer.api.campaign.TextPanelAPI; 015import com.fs.starfarer.api.campaign.econ.CommodityOnMarketAPI; 016import com.fs.starfarer.api.campaign.econ.MarketAPI; 017import com.fs.starfarer.api.characters.PersonAPI; 018import com.fs.starfarer.api.impl.campaign.ids.Conditions; 019import com.fs.starfarer.api.impl.campaign.ids.Factions; 020import com.fs.starfarer.api.impl.campaign.ids.Ranks; 021import com.fs.starfarer.api.impl.campaign.ids.Sounds; 022import com.fs.starfarer.api.impl.campaign.ids.Tags; 023import com.fs.starfarer.api.impl.campaign.rulecmd.AddRemoveCommodity; 024import com.fs.starfarer.api.impl.campaign.rulecmd.SetStoryOption; 025import com.fs.starfarer.api.impl.campaign.rulecmd.SetStoryOption.BaseOptionStoryPointActionDelegate; 026import com.fs.starfarer.api.impl.campaign.rulecmd.SetStoryOption.StoryOptionParams; 027import com.fs.starfarer.api.ui.TooltipMakerAPI; 028import com.fs.starfarer.api.util.Misc; 029import com.fs.starfarer.api.util.WeightedRandomPicker; 030 031public class DeliveryBarEvent extends BaseGetCommodityBarEvent { 032 033 public static String KEY_FAILED_RECENTLY = "$core_dmi_failedRecently"; 034 035 public static String KEY_SAW_DELIVERY_EVENT_RECENTLY = "$core_dmi_sawRecently"; 036 public static String KEY_ACCEPTED_AT_THIS_MARKET_RECENTLY = "$core_dmi_acceptedAtThisMarket"; 037 038 public static float PROB_HIGHER_CAPACITY = 0.25f; 039 040 public static float FAILED_RECENTLY_DURATION = 365f; 041 public static float SAW_RECENTLY_DURATION = 180f; 042 public static float ACCEPTED_AT_THIS_MARKET_DURATION = 90f; 043 044 public static float PROB_TO_SHOW = 0.5f; 045 046 protected MarketAPI destination; 047 protected int reward; 048 protected int escrow; 049 protected float duration; 050 protected FactionAPI faction; 051 protected int playerCargoCap = 0; 052 protected int playerFuelCap = 0; 053 054 protected DestinationData data; 055 056 public DeliveryBarEvent() { 057 super(); 058 } 059 060 public boolean shouldShowAtMarket(MarketAPI market) { 061 //if (true) return true; 062 if (!super.shouldShowAtMarket(market)) return false; 063 064 if (market.getFactionId().equals(Factions.PIRATES)) return false; 065 if (market.getFactionId().equals(Factions.LUDDIC_PATH)) return false; 066 067 // what we want: 068 // 1) don't show at the same market for a while 069 // 2) randomly don't show at any particular market, 070 // unless the player hasn't seen any delivery events in a while 071 072 if (market.getMemoryWithoutUpdate().getBoolean(KEY_ACCEPTED_AT_THIS_MARKET_RECENTLY)) { 073 return false; 074 } 075 076 regen(market); 077 078 if (destination == null) return false; 079 080 if (escrow > 0 && market.isPlayerOwned()) return false; 081 082 if (Global.getSector().getMemoryWithoutUpdate().getBoolean(KEY_SAW_DELIVERY_EVENT_RECENTLY) 083 && shownAt != market) { 084 if (random.nextFloat() > PROB_TO_SHOW) return false; 085 } 086 087 Global.getSector().getMemoryWithoutUpdate().set(KEY_SAW_DELIVERY_EVENT_RECENTLY, true, 088 SAW_RECENTLY_DURATION); 089 090 return true; 091 } 092 093 @Override 094 protected void regen(MarketAPI market) { 095 //if (this.market == market) return; 096 if (this.market != market) { 097 playerCargoCap = 0; 098 playerFuelCap = 0; 099 } 100 101 this.market = market; 102 random = new Random(seed + market.getId().hashCode()); 103 //random = Misc.random; 104 105 computeData(market); 106 107 if (destination != null) { 108 person = createPerson(); 109 } 110 } 111 112 public static class DestinationData { 113 public MarketAPI dest; 114 public CommodityOnMarketAPI comFrom; 115 public CommodityOnMarketAPI com; 116 public float distLY; 117 118 public boolean fromHasPA = false; 119 public boolean fromHasCells = false; 120 121 public boolean hasPA = false; 122 public boolean hasCells = false; 123 124 public boolean illegal = false; 125 126 public float score = 0; 127 128 public DestinationData(MarketAPI from, MarketAPI dest, 129 CommodityOnMarketAPI comFrom, CommodityOnMarketAPI comDest) { 130 this.dest = dest; 131 this.comFrom = comFrom; 132 this.com = comDest; 133 distLY = Misc.getDistanceLY(from.getLocationInHyperspace(), dest.getLocationInHyperspace()); 134 135 fromHasPA = from.hasCondition(Conditions.PIRATE_ACTIVITY); 136 fromHasCells = from.hasCondition(Conditions.PATHER_CELLS); 137 hasPA = dest.hasCondition(Conditions.PIRATE_ACTIVITY); 138 hasCells = dest.hasCondition(Conditions.PATHER_CELLS); 139 140 illegal = dest.isIllegal(com.getId()); 141 142 score += Math.min(distLY, 10f); 143 144 if (fromHasPA) score += 10f; 145 if (fromHasCells) score += 5f; 146 if (hasPA) score += 10f; 147 if (hasCells) score += 5f; 148 149// score += (comFrom.getAvailable() + comFrom.getMaxSupply()) * 0.5f; 150// score += comDest.getMaxDemand(); 151 152 } 153 } 154 155 protected void computeData(MarketAPI market) { 156 157 data = null; 158 destination = null; 159 reward = 0; 160 duration = 0; 161 faction = null; 162 quantity = 0; 163 commodity = null; 164 165 List<CommodityOnMarketAPI> commodities = new ArrayList<CommodityOnMarketAPI>(); 166 for (CommodityOnMarketAPI com : market.getCommoditiesCopy()) { 167 if (com.isNonEcon()) continue; 168 if (com.isMeta()) continue; 169 if (com.isPersonnel()) continue; 170 if (com.isIllegal()) continue; 171 172 if (com.getAvailable() <= 0) continue; 173 if (com.getMaxSupply() <= 0) continue; 174 175 commodities.add(com); 176 } 177 178 List<DestinationData> potential = new ArrayList<DestinationData>(); 179 180 float maxScore = 0; 181 float maxDist = 0; 182 for (MarketAPI other : Global.getSector().getEconomy().getMarketsCopy()) { 183 if (other == market) continue; 184 if (other.isHidden()) continue; 185 if (other.isInvalidMissionTarget()) continue; 186 187 if (other.getEconGroup() == null && market.getEconGroup() != null) continue; 188 if (other.getEconGroup() != null && !other.getEconGroup().equals(market.getEconGroup())) continue; 189 190 if (other.getStarSystem() == null) continue; 191 192 //WeightedRandomPicker<T> 193 for (CommodityOnMarketAPI com : commodities) { 194 //CommodityOnMarketAPI otherCom = other.getCommodityData(com.getId()); 195 CommodityOnMarketAPI otherCom = other.getCommodityData(com.getDemandClass()); 196 if (otherCom.getMaxDemand() <= 0) continue; 197 198 DestinationData data = new DestinationData(market, other, com, otherCom); 199 if (data.illegal) continue; 200 if (data.score > maxScore) { 201 maxScore = data.score; 202 } 203 if (data.distLY > maxDist) { 204 maxDist = data.distLY; 205 } 206 potential.add(data); 207 } 208 } 209 if (maxDist > 10) maxDist = 10; 210 211 WeightedRandomPicker<DestinationData> picker = new WeightedRandomPicker<DestinationData>(random); 212 for (int i = 0; i < potential.size(); i++) { 213 DestinationData d = potential.get(i); 214 if (d.score > maxScore * 0.5f && d.distLY > maxDist * 0.5f) { 215 picker.add(d, d.score * d.score * d.score); 216 } 217 } 218 219// Collections.sort(potential, new Comparator<DestinationData>() { 220// public int compare(DestinationData o1, DestinationData o2) { 221// return (int) Math.signum(o2.score - o1.score); 222// } 223// }); 224// 225// 226// WeightedRandomPicker<DestinationData> picker = new WeightedRandomPicker<DestinationData>(random); 227// for (int i = 0; i < potential.size() && i < 5; i++) { 228// DestinationData d = potential.get(i); 229// picker.add(d, d.score * d.score * d.score); 230// } 231 232 DestinationData pick = picker.pick(); 233 234 if (pick == null) return; 235 236 destination = pick.dest; 237 duration = pick.distLY * 5 + 50; 238 duration = (int)duration / 10 * 10; 239 240 CargoAPI cargo = Global.getSector().getPlayerFleet().getCargo(); 241 quantity = (int) cargo.getMaxCapacity(); 242 if (pick.com.isFuel()) { 243 quantity = (int) cargo.getMaxFuel(); 244 } 245 246 if (random.nextFloat() < PROB_HIGHER_CAPACITY) { 247 quantity *= 1f + random.nextFloat() * 3f; 248 quantity = (int) quantity; 249 } 250 251 // don't want mission at market to update quantity as player changes their fleet up 252 if (pick.com.isFuel()) { 253 if (playerFuelCap == 0) { 254 playerFuelCap = quantity; 255 } else { 256 quantity = playerFuelCap; 257 } 258 } else { 259 if (playerCargoCap == 0) { 260 playerCargoCap = quantity; 261 } else { 262 quantity = playerCargoCap; 263 } 264 } 265 266 quantity *= 0.5f + 0.25f * random.nextFloat(); 267 268 // somewhat less of the more valuable stuff 269 quantity *= Math.min(1f, 200f / pick.comFrom.getCommodity().getBasePrice()); 270 271 int limit = (int) (pick.comFrom.getAvailable() * pick.comFrom.getCommodity().getEconUnit()); 272 limit *= 0.75f + 0.5f * random.nextFloat(); 273 //if (quantity > 5000) quantity = 5000; 274 if (quantity > limit) quantity = limit; 275 276 277 if (quantity > 10000) quantity = quantity / 1000 * 1000; 278 else if (quantity > 100) quantity = quantity / 10 * 10; 279 else if (quantity > 10) quantity = quantity / 10 * 10; 280 281 if (quantity < 10) quantity = 10; 282 283 284 //float base = pick.com.getMarket().getSupplyPrice(pick.com.getId(), 1, true); 285 float base = pick.comFrom.getMarket().getSupplyPrice(pick.comFrom.getId(), 1, true); 286 287 if (quantity * base < 4000) { 288 base = Math.min(100, 4000 / quantity); 289 } 290 291// float minBase = 100; 292// if (quantity > 500) { 293// minBase = 50; 294// } 295 float minBase = 100f - 50f * Math.min(1f, quantity / 500f); 296 minBase = (base + minBase) * 0.75f; 297 298 if (base < minBase) base = minBase; 299 300 //float extra = 2000; 301 302 float mult = pick.score / 30f; 303 //if (market.isPlayerOwned() && mult > 2f) mult = 2f; 304 //if (market.isPlayerOwned()) mult *= 0.75f; 305 306 if (mult < 0.75f) mult = 0.75f; 307 //if (mult > 2) mult = 2; 308 reward = (int) (base * mult * quantity); 309 310// float minPerUnit = 50; 311// if (reward / quantity < minPerUnit) { 312// reward = (int) (minPerUnit * quantity); 313// } 314 315 reward = reward / 1000 * 1000; 316 if (reward < 4000) reward = 4000; 317 318 319 if (Global.getSector().getMemoryWithoutUpdate().getBoolean(KEY_FAILED_RECENTLY)) { 320 escrow = (int) (quantity * pick.comFrom.getCommodity().getBasePrice()); 321 } 322 323 324 if (market.getFaction() == pick.dest.getFaction()) { 325 faction = market.getFaction(); 326 } else { 327 faction = Global.getSector().getFaction(Factions.INDEPENDENT); 328 if (faction == null) faction = market.getFaction(); 329 } 330 331 commodity = pick.comFrom.getId(); 332 333 data = pick; 334 } 335 336 protected int getNegotiatedAmount() { 337 return (int) (reward * 1.5f); 338 } 339 340 protected void addStoryOption() { 341 String id = "negotiate_id"; 342 options.addOption("Negotiate a higher fee for the delivery", id); 343 344 StoryOptionParams params = new StoryOptionParams(id, 1, "negotiateDeliveryFee", Sounds.STORY_POINT_SPEND_INDUSTRY, 345 "Negotiated higher fee for delivery of " + data.comFrom.getCommodity().getLowerCaseName() + " to " + data.dest.getName()); 346 347 SetStoryOption.set(dialog, params, 348 new BaseOptionStoryPointActionDelegate(dialog, params) { 349 350 @Override 351 public void confirm() { 352 super.confirm(); 353 reward = getNegotiatedAmount(); 354 dialog.getTextPanel().addPara(getNegotiatedText()); 355 OptionPanelAPI options = dialog.getOptionPanel(); 356 options.clearOptions(); 357 options.addOption("Continue", OPTION_CONFIRM); 358 //optionSelected(null, OPTION_CONFIRM); 359 } 360 361 @Override 362 public String getTitle() { 363 //return "Negotiating delivery fee"; 364 return null; 365 } 366 367 @Override 368 public void createDescription(TooltipMakerAPI info) { 369 float opad = 10f; 370 371 info.addSpacer(-opad); 372 373 info.setParaInsigniaLarge(); 374 info.addPara("You're able to negotiate the delivery fee from %s up to " + 375 "%s.", 0f, Misc.getHighlightColor(), 376 Misc.getDGSCredits(reward), 377 Misc.getDGSCredits(getNegotiatedAmount())); 378 379 info.addSpacer(opad * 2f); 380 addActionCostSection(info); 381 } 382 383 }); 384 } 385 386 @Override 387 protected boolean canAccept() { 388 if (escrow <= 0) return true; 389 float credits = Global.getSector().getPlayerFleet().getCargo().getCredits().get(); 390 boolean canAfford = credits >= escrow; 391 return canAfford; 392 } 393 394 @Override 395 protected void doStandardConfirmActions() { 396 CargoAPI cargo = Global.getSector().getPlayerFleet().getCargo(); 397 TextPanelAPI text = dialog.getTextPanel(); 398 399 cargo.addCommodity(commodity, quantity); 400 AddRemoveCommodity.addCommodityGainText(commodity, quantity, text); 401 402 if (escrow > 0) { 403 cargo.getCredits().subtract(escrow); 404 AddRemoveCommodity.addCreditsLossText(escrow, text); 405 } 406 407 createIntel(); 408 } 409 410 protected void createIntel() { 411 DeliveryMissionIntel intel = new DeliveryMissionIntel(this, dialog); 412 413 market.getMemoryWithoutUpdate().set(KEY_ACCEPTED_AT_THIS_MARKET_RECENTLY, true, 414 ACCEPTED_AT_THIS_MARKET_DURATION * (0.75f + random.nextFloat() * 0.5f)); 415 } 416 417 @Override 418 protected void adjustPerson(PersonAPI person) { 419 super.adjustPerson(person); 420 person.setImportanceAndVoice(pickImportance(), random); 421 person.addTag(Tags.CONTACT_TRADE); 422 } 423 424 @Override 425 protected String getPersonFaction() { 426 return faction.getId(); 427 } 428 429 @Override 430 protected String getPersonRank() { 431 return Ranks.CITIZEN; 432 } 433 434 @Override 435 protected String getPersonPost() { 436 //return Ranks.CITIZEN; 437 return pickOne(Ranks.POST_TRADER, Ranks.POST_COMMODITIES_AGENT, 438 Ranks.POST_MERCHANT, Ranks.POST_INVESTOR); 439 } 440 441 @Override 442 protected float getPriceMult() { 443 return 0; 444 } 445 446 @Override 447 protected String getPrompt() { 448 if (faction.getId().equals(Factions.INDEPENDENT)) { 449 return "At a corner table, a concerned-looking " + getManOrWoman() + 450 " glumly examines " + getHisOrHer() + " TriPad."; 451 } else { 452 return "At a corner table, a concerned-looking " + getManOrWoman() + 453 " in " + faction.getPersonNamePrefixAOrAn() + " " + faction.getPersonNamePrefix() + " uniform " + 454 " glumly examines " + getHisOrHer() + " TriPad."; 455 } 456 } 457 458 @Override 459 protected String getOptionText() { 460 return "Nod to the concerned " + getManOrWoman() + " and walk over to " + getHisOrHer() + " table"; 461 } 462 463 @Override 464 protected String getMainText() { 465 String str = ""; 466 if (market.isPlayerOwned()) { 467 String sir = "sir"; 468 if (Global.getSector().getPlayerPerson().isFemale()) sir = "ma'am"; 469 str = "\"Oh, it's you, " + sir + "!\", " + getHeOrShe() + 470 " exclaims. Taking a moment to recover " + getHisOrHer() + " composure, " + getHeOrShe() + " says \"We've got a little logistical problem that could use " + 471 "your personal touch. " + 472 "There are %s units of " + data.comFrom.getCommodity().getLowerCaseName() + " that urgently need to be delivered " + 473 " to %s" + 474 ", in the " + data.dest.getStarSystem().getNameWithLowercaseType() + ". "; 475 if (data.fromHasPA || data.hasPA) { 476 str += "However, recent pirate activity has been making that difficult, and the regular trade fleets " + 477 "aren't quite up to the task.\""; 478 } else if (data.fromHasCells || data.hasCells) { 479 str += "However, recent Pather cell activity has been making that difficult, and the regular trade fleets " + 480 "aren't quite up to the task.\""; 481 } else { 482 str += "But, well, you know what trader captains are like. " + 483 "There have been some disagreements over hazard pay, and it's left us in the lurch.\""; 484 } 485 } else { 486 str = "After a brief introduction, " + getHeOrShe() + " wastes no time in getting to the point.\n\n" + 487 "\"I've got %s units of " + data.comFrom.getCommodity().getLowerCaseName() + " that urgently need to be delivered " + 488 " to %s" + 489 ", in the " + data.dest.getStarSystem().getNameWithLowercaseType() + ". "; 490 if (data.fromHasPA || data.hasPA) { 491 str += "Recent pirate activity has been making that difficult, but you look like someone that could " + 492 "get the job done.\""; 493 } else if (data.fromHasCells || data.hasCells) { 494 str += "Recent Pather cell activity has been making that difficult, but I'm sure you can handle " + 495 "any trouble.\""; 496 } else { 497 str += "We've had some disputes with the regular shipping company, and it's left us in the lurch. " + 498 "Should be a milk run for someone like you, though.\""; 499 } 500 } 501 502 //str += "\n\nYou recall that " + data.dest.getName() + " is under %s control, and %s light-years away. "; 503 504 String where = "located in hyperspace,"; 505 if (data.dest.getStarSystem() != null) { 506 //where = "located in the " + data.dest.getStarSystem().getNameWithLowercaseType() + ", which is"; 507 where = "located in the " + data.dest.getStarSystem().getNameWithLowercaseType() + ""; 508 } 509 //str += "\n\nYou recall that " + data.dest.getName() + " is under %s control, and " + where + " %s light-years away. "; 510 str += "\n\nYou recall that " + data.dest.getName() + " is under %s control, and " + where + "."; 511 512 CargoAPI cargo = Global.getSector().getPlayerFleet().getCargo(); 513 if (data.comFrom.isFuel()) { 514 int cap = cargo.getFreeFuelSpace(); 515 if (cap > 1) { 516 str += " Your fleet's fuel tanks can hold an additional %s units of fuel."; 517 } else { 518 str += "%sYour fleet's fuel tanks are currently full."; // %s - need to have same number of highlights 519 } 520 } else { 521 int cap = (int) cargo.getSpaceLeft(); 522 if (cap > 1) { 523 str += " Your fleet's holds can accommodate an additional %s units of cargo."; 524 } else { 525 str += "%sYour fleet's cargo holds are currently full."; 526 } 527 } 528 529 if (market.isPlayerOwned()) { 530 str += "\n\n" + Misc.ucFirst(getHeOrShe()) + " double-checks something on " + getHisOrHer() + " pad. " + 531 "\"The customer will pay %s upon delivery within %s days. Can you take this on?\""; 532 } else { 533 if (escrow > 0) { 534 str += "\n\n" + Misc.ucFirst(getHeOrShe()) + " double-checks something on " + getHisOrHer() + " pad. " + 535 "\"The offer is %s credits, payable upon delivery within %s days. You'll also have to " + 536 "transfer %s to an escrow account. This will be returned to you " + 537 "when you complete the delivery - " + 538 "standard insurance procedure, you understand.\""; 539 } else { 540 str += "\n\n" + Misc.ucFirst(getHeOrShe()) + " double-checks something on " + getHisOrHer() + " pad. " + 541 "\"The offer is %s credits, payable upon delivery within %s days. You in?\""; 542 } 543 } 544 545 return str; 546 } 547 548 @Override 549 protected String [] getMainTextTokens() { 550 CargoAPI cargo = Global.getSector().getPlayerFleet().getCargo(); 551 int cap = 0; 552 if (data.comFrom.isFuel()) { 553 cap = cargo.getFreeFuelSpace(); 554 } else { 555 cap = (int) cargo.getSpaceLeft(); 556 } 557 return new String [] { Misc.getWithDGS(quantity), data.dest.getName(), 558 data.dest.getFaction().getPersonNamePrefix(), 559 //Misc.getRoundedValueMaxOneAfterDecimal(data.distLY), 560 cap > 1 ? Misc.getWithDGS(cap) : " ", 561 Misc.getDGSCredits(reward), 562 Misc.getWithDGS(duration), 563 Misc.getDGSCredits(escrow) }; 564 } 565 @Override 566 protected Color [] getMainTextColors() { 567 return new Color [] { Misc.getHighlightColor(), 568 //data.dest.getFaction().getBaseUIColor(), 569 Misc.getTextColor(), 570 data.dest.getFaction().getBaseUIColor(), 571 //Misc.getHighlightColor(), 572 Misc.getHighlightColor(), 573 Misc.getHighlightColor(), 574 Misc.getHighlightColor(), 575 Misc.getHighlightColor() }; 576 } 577 578 @Override 579 protected String getConfirmText() { 580 if (market.isPlayerOwned()) { 581 return "Agree to handle the contract"; 582 } 583 return "Accept the delivery contract"; 584 } 585 586 protected String getNegotiatedText() { 587 return "\"You drive a hard bargain! Very well, it's a deal.\" " + Misc.ucFirst(getHeOrShe()) + 588 " does not appear too displeased. You consider that the initial offer " + 589 "was probably on the low side."; 590 } 591 592 @Override 593 protected String getCancelText() { 594 if (market.isPlayerOwned()) { 595 return "Decline, explaining that you've got other urgent business to attend to"; 596 } 597 return "Decline the offer, explaining that you've got other plans"; 598 } 599 600 @Override 601 protected String getAcceptText() { 602 return "You receive authorization codes to access a dockside warehouse, and " + 603 "contact your quartermaster with instructions to begin loading the cargo."; 604 } 605 606 607 public MarketAPI getDestination() { 608 return destination; 609 } 610 611 public int getReward() { 612 return reward; 613 } 614 615 public float getDuration() { 616 return duration; 617 } 618 619 public FactionAPI getFaction() { 620 return faction; 621 } 622 623 public DestinationData getData() { 624 return data; 625 } 626 627 public int getQuantity() { 628 return quantity; 629 } 630 631 public int getEscrow() { 632 return escrow; 633 } 634 635 protected boolean showCargoCap() { 636 return false; 637 } 638 639 @Override 640 protected void showTotalAndOptions() { 641 super.showTotalAndOptions(); 642 643 String icon = Global.getSettings().getCommoditySpec(commodity).getIconName(); 644 String text = null; 645 Set<String> tags = new LinkedHashSet<String>(); 646 tags.add(Tags.INTEL_MISSIONS); 647 648 dialog.getVisualPanel().showMapMarker(getDestination().getPrimaryEntity(), 649 "Destination: " + getDestination().getName(), getDestination().getFaction().getBaseUIColor(), 650 true, icon, text, tags); 651 } 652 653 654 655} 656 657 658