Dan Albert df80ec635f Add a new miz file based campaign generator.
Defining a campaign using a miz file instead of as JSON has a number of
advantages:

* Much easier for players to mod their campaigns.
* Easier to see the big picture of how objective locations will be laid
  out, since every control point can be seen at once.
* No need to associate objective locations to control points explicitly;
  the campaign generator can claim objectives for control points based
  on distance.
* Easier to create an IADS that performs well.
* Non-random campaigns are easier to make.

The downside is duplication across campaigns, and a less structured data
format for complex objects. The former is annoying if we have to fix a
bug that appears in a dozen campaigns. It's less an annoyance for
needing to start from scratch since the easiest way to create a campaign
will be to copy the "full" campaign for the given theater and prune it.

So far I've implemented control points, base defenses, and front lines.
Still need to add support for non-base defense TGOs.

This currently doesn't do anything for the `radials` property of the
`ControlPoint` because I'm not sure what those are.
2020-11-19 16:55:21 -08:00

90 lines
2.8 KiB
Python

from __future__ import annotations
import json
import logging
from dataclasses import dataclass
from pathlib import Path
from typing import List
from PySide2 import QtGui
from PySide2.QtCore import QItemSelectionModel
from PySide2.QtGui import QStandardItem, QStandardItemModel
from PySide2.QtWidgets import QAbstractItemView, QListView
import qt_ui.uiconstants as CONST
from theater import ConflictTheater
@dataclass(frozen=True)
class Campaign:
name: str
icon_name: str
authors: str
description: str
theater: ConflictTheater
@classmethod
def from_json(cls, path: Path) -> Campaign:
with path.open() as campaign_file:
data = json.load(campaign_file)
sanitized_theater = data["theater"].replace(" ", "")
return cls(data["name"], f"Terrain_{sanitized_theater}",
data.get("authors", "???"),
data.get("description", ""),
ConflictTheater.from_json(path.parent, data))
def load_campaigns() -> List[Campaign]:
campaign_dir = Path("resources\\campaigns")
campaigns = []
for path in campaign_dir.iterdir():
try:
logging.debug(f"Loading campaign from {path}...")
campaign = Campaign.from_json(path)
campaigns.append(campaign)
except RuntimeError:
logging.exception(f"Unable to load campaign from {path}")
return sorted(campaigns, key=lambda x: x.name)
class QCampaignItem(QStandardItem):
def __init__(self, campaign: Campaign) -> None:
super(QCampaignItem, self).__init__()
self.setIcon(QtGui.QIcon(CONST.ICONS[campaign.icon_name]))
self.setEditable(False)
self.setText(campaign.name)
class QCampaignList(QListView):
def __init__(self, campaigns: List[Campaign]) -> None:
super(QCampaignList, self).__init__()
self.model = QStandardItemModel(self)
self.setModel(self.model)
self.setMinimumWidth(250)
self.setMinimumHeight(350)
self.campaigns = []
self.setSelectionBehavior(QAbstractItemView.SelectItems)
self.setup_content(campaigns)
def setup_content(self, campaigns: List[Campaign]) -> None:
for campaign in campaigns:
self.campaigns.append(campaign)
item = QCampaignItem(campaign)
self.model.appendRow(item)
self.setSelectedCampaign(0)
self.repaint()
def setSelectedCampaign(self, row):
self.selectionModel().clearSelection()
index = self.model.index(row, 0)
if not index.isValid():
index = self.model.index(0, 0)
self.selectionModel().setCurrentIndex(index, QItemSelectionModel.Select)
self.repaint()
def clear_layout(self):
self.model.removeRows(0, self.model.rowCount())