forked from rcav8tr/CS1Mod-PopulationDemographics
-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathPopulationDemographicsPanel.cs
2538 lines (2277 loc) · 137 KB
/
PopulationDemographicsPanel.cs
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
using ColossalFramework.UI;
using ColossalFramework.Globalization;
using UnityEngine;
using System;
using System.Collections.Generic;
using System.Threading;
namespace PopulationDemographics
{
/// <summary>
/// a panel to display demographics on the screen
/// </summary>
public class PopulationDemographicsPanel : UIPanel
{
// age constants
private const int MaxGameAge = 400; // obtained from Citizen.Age
private const float RealAgePerGameAge = 1f / 3.5f; // obtained from District.GetAverageLifespan
private const int MaxRealAge = (int)(MaxGameAge * RealAgePerGameAge);
/// <summary>
/// attributes for a row or column selection
/// </summary>
private class SelectionAttributes
{
public string selectionText;
public string[] headingTexts;
public Color32[] amountBarColors; // only rows (not columns) have amount bars
public SelectionAttributes(string selectionText, string[] headingTexts, Color32[] amountBarColors)
{
this.selectionText = selectionText;
this.headingTexts = headingTexts;
this.amountBarColors = amountBarColors;
}
}
// row selections
public enum RowSelection
{
Age,
AgeGroup,
Education,
Employment,
Gender,
Happiness,
Health,
Location,
Residential,
Student,
Wealth,
WellBeing
}
// attributes for each row selection
private class RowSelectionAttributes : Dictionary<RowSelection, SelectionAttributes> { }
private RowSelectionAttributes _rowSelectionAttributes;
// maximum number of rows is the number of Ages which goes from 0 to MaxRealAge inclusive
private const int MaxRows = MaxRealAge + 1;
// column selections
public enum ColumnSelection
{
None,
AgeGroup,
Education,
Employment,
Gender,
Happiness,
Health,
Location,
Residential,
Student,
Wealth,
WellBeing
}
// attributes for each column selection
private class ColumnSelectionAttributes : Dictionary<ColumnSelection, SelectionAttributes> { }
private ColumnSelectionAttributes _columnSelectionAttributes;
// maximum number of columns is the number of heading texts for Health which has the most headings
private const int MaxColumns = 6;
// text colors
private static readonly Color32 TextColorNormal = new Color32(185, 221, 254, 255);
private const float LockedColorMultiplier = 0.5f;
private static readonly Color32 TextColorLocked = new Color32((byte)(TextColorNormal.r * LockedColorMultiplier), (byte)(TextColorNormal.g * LockedColorMultiplier), (byte)(TextColorNormal.b * LockedColorMultiplier), 255);
private const float LineColorMultiplier = 0.8f;
private static readonly Color32 LineColor = new Color32((byte)(TextColorNormal.r * LineColorMultiplier), (byte)(TextColorNormal.g * LineColorMultiplier), (byte)(TextColorNormal.b * LineColorMultiplier), 255);
// text scales
private const float TextScaleHeading = 0.625f;
private const float TextScale = 0.75f;
private const float TextScaleAge = 0.625f;
// UI widths
private const float PaddingWidth = 10f; // padding around left and right edges of panel and between row/column selections and data rows
private const float SelectionWidth = 90f; // width of row/column selections
private const float DescriptionWidth = 100f; // width of description label
private const float AmountWidth = 67f; // width of each amount label (just large enough to hold 7 digits with grouping symbols)
private const float AmountSpacing = 4f; // spacing between amounts
private const float AmountSpacingAfterTotal = 16f; // spacing between total amount and moving in amount
private const float ScrollbarWidth = 16f; // width of scroll bar
private const float DataWidth = // width of data
DescriptionWidth + // data description
MaxColumns * AmountSpacing + // spacing before each amount
MaxColumns * AmountWidth + // amounts
AmountSpacing + // spacing before total
AmountWidth + // total
AmountSpacingAfterTotal + // spacing after total
AmountWidth + // moving in
AmountSpacing + // spacing after moving in
AmountWidth + // deceased
AmountSpacing + // spacing after deceased
ScrollbarWidth; // scroll bar
private const float PanelTotalWidth = // total width of the demographics panel
PaddingWidth + // padding between panel left edge and row/column selections
SelectionWidth + // row/column selections
PaddingWidth + // padding between row/column selections and data rows
DataWidth + // width of data rows
PaddingWidth; // padding between data rows and panel right edge
// UI heights
private const float TitleBarHeight = 40f; // height of title bar in MenuPanel2 sprite
private const float PaddingTop = 5f; // padding between title bar and district drop down
private const float PaddingHeight = 10f; // padding around bottom edge of panel and vertical space between UI components
private const float DistrictHeight = 45f; // height of district drop down
private const float DistrictItemHeight = 17f; // height of items in district drop down list
private const float HeadingTop = // top of heading panel (and row selection label)
TitleBarHeight + // title bar
PaddingTop + // spacing after title bar
DistrictHeight + // district drop down
PaddingHeight; // after district drop down
private const float TextHeight = 15f; // height of data row
private const float TextHeightAge = 10f; // height of data row for Age
private const float SpaceAfterTotalRow = 12f; // space between total row and moving in row
private const float HeightOfTotals = // height of totals section
4f + // lines before total row
TextHeight + // total row
SpaceAfterTotalRow + // space after totals
TextHeight + // moving in row
TextHeight; // deceased row
private const float SpaceAfterTotalsSection = 10f; // space between totals section and legend section
private const float HeightOfLegend = TextHeight; // height of legend section
private float _panelHeightNotAge; // panel height when Age is not selected
private const float PanelHeightForAge = 1000f; // panel height when Age is selected
/// <summary>
/// UI elements for one row of data
/// the UI components for one data row are placed on this panel, each data row on its own panel
/// </summary>
private class DataRowUI : UIPanel
{
public UILabel description;
public UISprite amountBar;
public UILabel[] amount = new UILabel[MaxColumns];
public UILabel total;
public UILabel movingIn;
public UILabel deceased;
}
/// <summary>
/// UI elements for one row of lines
/// </summary>
private class LinesRowUI
{
// no line for description/amount bar
// lines for: amounts, total, moving in, and deceased
public UISprite[] amount = new UISprite[MaxColumns];
public UISprite total;
public UISprite movingIn;
public UISprite deceased;
}
// UI elements that get adjusted based on user selections
private DataRowUI _heading;
private LinesRowUI _headingLines;
private UIPanel _dataPanel;
private UIScrollablePanel _dataScrollablePanel;
private UIPanel _dataRowsPanel;
private DataRowUI[] _dataRows;
private LinesRowUI _totalLines;
private DataRowUI _totalRow;
private DataRowUI _movingInRow;
private DataRowUI _deceasedRow;
private UILabel _legendLowValue;
private UILabel _legendHighValue;
// UI elements for count/percent buttons
private UIPanel _countPanel;
private UIPanel _percentPanel;
private UISprite _countCheckBox;
private UISprite _percentCheckBox;
// UI elements for opacity
public const float DefaultOpacity = 1f;
private UISlider _opacitySlider;
private UILabel _opacityValueLabel;
// here is the hierarchy of UI elements:
//
// PopulationDemographicsPanel
// population icon
// title label
// close button
// district dropdown
// opacity label and slider
// row selection label and listbox
// column selection label and listbox
// heading panel
// DataRowUI for heading row
// LinesRowUI below headings
// data panel
// data scrollable panel
// data rows panel
// DataRowUI's for MaxRows data rows
// vertical scroll bar for scrollable panel
// scroll bar track
// scroll bar thumb
// total panel
// LinesRowUI above total row
// DataRowUI for total row
// DataRowUI for moving in row
// DataRowUI for deceased row
// display option panel for count
// count checkbox sprite
// count label
// display option panel for percent
// percent checkbox sprite
// percent label
// legend panel
// low value label
// color gradient
// high value label
// employment status
private enum EmploymentStatus
{
Student,
Employed,
Unemployed
}
/// <summary>
/// the demographic data for one citizen
/// </summary>
private class CitizenDemographic
{
public uint citizenID;
public byte districtID;
public bool deceased;
public bool movingIn;
public int age; // real age, not game age
public Citizen.AgeGroup ageGroup;
public Citizen.Education education;
public EmploymentStatus employment;
public Citizen.Gender gender;
public Citizen.Happiness happiness;
public Citizen.Health health;
public Citizen.Location location; // Hotel location is not used because only tourists, not citizens, are guests at hotels
public ItemClass.Level residential; // None (i.e. -1) should never happen because Levels 1-5 (i.e. 0-4) are level of citizen's home building
public ItemClass.Level student; // None (i.e. -1) = not a student, Levels 1-3 (i.e. 0-2) = Elementary, High School, University
public Citizen.Wealth wealth;
public Citizen.Wellbeing wellbeing;
}
/// <summary>
/// the demographic data for a collection of citizens
/// </summary>
private class CitizenDemographics : List<CitizenDemographic> { }
// the citizen demographics buffers
// temp buffer gets populated in simulation thread
// final buffer gets updated periodically from temp buffer in simulation thread and is used in UI thread to display the demographics
private CitizenDemographics _tempCitizens;
private CitizenDemographics _finalCitizens;
/// <summary>
/// the demographic data for one building
/// </summary>
private class BuildingDemographic
{
public byte districtID;
public int citizenCount;
public bool isValid;
public float avgAge; // real age, not game age
public float avgEducation;
public int unemployedCount;
public int jobEligibleCount;
public float avgUnemployed; // unemployment rate
public float avgGender;
public float avgHappiness;
public float avgHealth;
public int atHomeCount;
public float avgAtHome; // Location
public int residential;
public int studentCount;
public float avgStudent;
public float avgWealth;
public float avgWellbeing;
}
// define a min and max for each building average
private int _minCitizens;
private int _maxCitizens;
private float _minAge;
private float _maxAge;
private float _minEducation;
private float _maxEducation;
private float _minUnemployed;
private float _maxUnemployed;
private float _minGender;
private float _maxGender;
private float _minHappiness;
private float _maxHappiness;
private float _minHealth;
private float _maxHealth;
private float _minAtHome;
private float _maxAtHome;
private int _minResidential;
private int _maxResidential;
private float _minStudent;
private float _maxStudent;
private float _minWealth;
private float _maxWealth;
private float _minWellBeing;
private float _maxWellBeing;
// the building demographic buffers, one for each possible building
// temp buffer gets populated in simulation thread
// final buffer gets updated periodically from temp buffer in simulation thread and is used by UI thread to display the demographics
private BuildingDemographic[] _tempBuildings;
private BuildingDemographic[] _finalBuildings;
// building colors
private static Color _neutralColor;
private static Color _buildingColorLow;
private static Color _buildingColorHigh;
// for locking the thread while working with final buffer that is used by both the simulation thread and the UI thread
private static readonly object _lockObject = new object();
// miscellaneous
private byte _selectedDistrictID = UIDistrictDropdown.DistrictIDEntireCity;
private bool _initialized = false;
private uint _citizenCounter = 0;
private bool _triggerUpdatePanel = false;
private UIPanel _populationLegend;
private bool _hadronColliderEnabled;
/// <summary>
/// amounts for one data row
/// </summary>
private class DataRow
{
// one amount for each data column
public int[] amount = new int[MaxColumns];
// amounts for total, moving in, and deceased columns
public int total;
public int movingIn;
public int deceased;
}
/// <summary>
/// Start is called after the panel is created in Loading
/// set up and populate the panel
/// </summary>
public override void Start()
{
// do base processing
base.Start();
try
{
// set panel properties
name = "PopulationDemographicsPanel";
backgroundSprite = "MenuPanel2";
canFocus = true;
// set initial visibility and opacity from config
Configuration config = ConfigurationUtil<Configuration>.Load();
isVisible = config.PanelVisible;
opacity = config.PanelOpacity;
// get the PopulationInfoViewPanel panel (displayed when the user clicks on the Population info view button)
PopulationInfoViewPanel populationPanel = UIView.library.Get<PopulationInfoViewPanel>(nameof(PopulationInfoViewPanel));
if (populationPanel == null)
{
LogUtil.LogError("Unable to find PopulationInfoViewPanel.");
return;
}
// get legend from Population panel
_populationLegend = populationPanel.Find<UIPanel>("Legend");
if (_populationLegend == null)
{
LogUtil.LogError("Unable to find Legend on PopulationInfoViewPanel.");
return;
}
// place panel to the right of PopulationInfoViewPanel
relativePosition = new Vector3(populationPanel.component.size.x - 1f, 0f);
// set panel to exact width to hold contained components
// must do this before setting anchors on contained components
autoSize = false;
width = PanelTotalWidth;
// get text font from the Population label because it is regular instead of semi-bold
UILabel populationLabel = populationPanel.Find<UILabel>("Population");
if (populationLabel == null)
{
LogUtil.LogError("Unable to find Population label on PopulationInfoViewPanel.");
return;
}
UIFont textFont = populationLabel.font;
// initialize row and column selection attributes
if (!InitializeRowColumnSelections())
{
return;
}
// get neutral color
if (!InfoManager.exists)
{
LogUtil.LogError("InfoManager is not ready.");
return;
}
_neutralColor = InfoManager.instance.m_properties.m_neutralColor;
// get building low and high colors
// high color is 50% between residential low and high density zone colors
// low color is 15% between neutral and the high color
if (!ZoneManager.exists)
{
LogUtil.LogError("ZoneManager is not ready.");
return;
}
Color[] zoneColors = ZoneManager.instance.m_properties.m_zoneColors;
_buildingColorHigh = Color.Lerp(zoneColors[(int)ItemClass.Zone.ResidentialLow], zoneColors[(int)ItemClass.Zone.ResidentialHigh], 0.5f);
_buildingColorLow = Color.Lerp(_neutralColor, _buildingColorHigh, 0.15f);
// for most of the UI elements added in the logic below,
// anchors are used to automatically resize or move the elements when the panel size changes
// the panel size changes based on row and column selections
// create the title label
UILabel title = AddUIComponent<UILabel>();
if (title == null)
{
LogUtil.LogError($"Unable to create title label.");
return;
}
title.name = "Title";
title.font = textFont;
title.text = "Demographics";
title.textAlignment = UIHorizontalAlignment.Center;
title.textScale = 1f;
title.textColor = new Color32(254, 254, 254, 255);
title.autoSize = false;
title.size = new Vector2(width, 18f);
title.relativePosition = new Vector3(0f, 11f);
title.anchor = UIAnchorStyle.Left | UIAnchorStyle.Top | UIAnchorStyle.Right;
// create population icon in upper left
UISprite panelIcon = AddUIComponent<UISprite>();
if (panelIcon == null)
{
LogUtil.LogError($"Unable to create population icon.");
return;
}
panelIcon.name = "PopulationIcon";
panelIcon.autoSize = false;
panelIcon.size = new Vector2(36f, 36f);
panelIcon.relativePosition = new Vector3(10f, 2f);
panelIcon.spriteName = "InfoIconPopulationPressed";
// create close button in upper right
UIButton closeButton = AddUIComponent<UIButton>();
if (closeButton == null)
{
LogUtil.LogError($"Unable to create close button.");
return;
}
closeButton.name = "CloseButton";
closeButton.autoSize = false;
closeButton.size = new Vector2(32f, 32f);
closeButton.relativePosition = new Vector3(width - 34f, 2f);
closeButton.anchor = UIAnchorStyle.Right | UIAnchorStyle.Top;
closeButton.normalBgSprite = "buttonclose";
closeButton.hoveredBgSprite = "buttonclosehover";
closeButton.pressedBgSprite = "buttonclosepressed";
// create district dropdown
UIDistrictDropdown district = AddUIComponent<UIDistrictDropdown>();
if (district == null || !district.initialized)
{
LogUtil.LogError($"Unable to create district dropdown.");
return;
}
district.name = "DistrictSelection";
district.dropdownHeight = DistrictItemHeight + 7f;
district.font = textFont;
district.textScale = TextScale;
district.textColor = TextColorNormal;
district.disabledTextColor = TextColorLocked;
district.listHeight = 10 * (int)DistrictItemHeight + 8;
district.itemHeight = (int)DistrictItemHeight;
district.builtinKeyNavigation = true;
district.relativePosition = new Vector3(PaddingWidth, TitleBarHeight + PaddingTop);
district.autoSize = false;
district.size = new Vector2(width - 2f * PaddingWidth, DistrictHeight);
district.anchor = UIAnchorStyle.Left | UIAnchorStyle.Top | UIAnchorStyle.Right;
_selectedDistrictID = UIDistrictDropdown.DistrictIDEntireCity;
district.selectedDistrictID = _selectedDistrictID;
// create opacity slider from template on district panel
if (!CreateOpacitySlider(district)) { return; }
// create row selection label
if (!CreateSelectionLabel(textFont, out UILabel rowSelectionLabel)) { return; }
rowSelectionLabel.name = "RowSelectionLabel";
rowSelectionLabel.text = "Row:";
rowSelectionLabel.relativePosition = new Vector3(PaddingWidth, HeadingTop);
// create row selection list
string[] rowTexts = new string[_rowSelectionAttributes.Count];
for (int r = 0; r < rowTexts.Length; r++)
{
rowTexts[r] = _rowSelectionAttributes[(RowSelection)r].selectionText;
}
if (!CreateSelectionListBox(textFont, rowTexts, out UIListBox rowSelectionListBox)) { return; }
rowSelectionListBox.name = "RowSelection";
rowSelectionListBox.relativePosition = new Vector3(PaddingWidth, rowSelectionLabel.relativePosition.y + rowSelectionLabel.size.y);
// create column selection label
if (!CreateSelectionLabel(textFont, out UILabel columnSelectionLabel)) { return; }
columnSelectionLabel.name = "ColumnSelectionLabel";
columnSelectionLabel.text = "Column:";
columnSelectionLabel.relativePosition = new Vector3(PaddingWidth, rowSelectionListBox.relativePosition.y + rowSelectionListBox.size.y + PaddingHeight);
// create column selection list
string[] columnTexts = new string[_columnSelectionAttributes.Count];
for (int c = 0; c < columnTexts.Length; c++)
{
columnTexts[c] = _columnSelectionAttributes[(ColumnSelection)c].selectionText;
}
if (!CreateSelectionListBox(textFont, columnTexts, out UIListBox columnSelectionListBox)) { return; }
columnSelectionListBox.name = "ColumnSelection";
columnSelectionListBox.relativePosition = new Vector3(PaddingWidth, columnSelectionLabel.relativePosition.y + columnSelectionLabel.size.y);
// set initial row and column selections from config
rowSelectionListBox .selectedIndex = Math.Min(config.RowSelection, rowSelectionListBox .items.Length - 1);
columnSelectionListBox.selectedIndex = Math.Min(config.ColumnSelection, columnSelectionListBox.items.Length - 1);
// set panel to exact height to be just below column list box
// must do this before setting anchors on subsequent contained components
height = columnSelectionListBox.relativePosition.y + columnSelectionListBox.size.y + PaddingHeight;
_panelHeightNotAge = height; // remember this height
// create panel to hold headings, heading lines, data scrollable panel, scroll bar, total lines, total row, moving in row, and deceased row
UIPanel headingPanel = AddUIComponent<UIPanel>();
if (headingPanel == null)
{
LogUtil.LogError($"Unable to create heading panel.");
return;
}
headingPanel.name = "HeadingPanel";
headingPanel.autoSize = false;
headingPanel.size = new Vector2(width - rowSelectionListBox.relativePosition.x - rowSelectionListBox.size.x - PaddingWidth - ScrollbarWidth - PaddingWidth, 50f);
headingPanel.relativePosition = new Vector3(rowSelectionListBox.relativePosition.x + rowSelectionListBox.size.x + PaddingWidth, rowSelectionLabel.relativePosition.y);
headingPanel.anchor = UIAnchorStyle.Left | UIAnchorStyle.Top | UIAnchorStyle.Right;
// create heading row UI on heading panel
if (!CreateDataRowUI(headingPanel, textFont, 0f, "HeadingDataRow", out _heading)) { return; }
// adjust heading properties
_heading.description.text = "";
_heading.total.text = "Total";
_heading.movingIn.text = "MovingIn";
_heading.deceased.text = "Deceased";
_heading.amountBar.isVisible = false;
foreach (UILabel heading in _heading.amount)
{
heading.textScale = TextScaleHeading;
}
_heading.total .textScale = TextScaleHeading;
_heading.movingIn.textScale = TextScaleHeading;
_heading.deceased.textScale = TextScaleHeading;
// create lines after headings
if (!CreateLines(headingPanel, 15f, "HeadingLine", out _headingLines)) { return; }
// set height of heading panel
headingPanel.height = _headingLines.total.relativePosition.y + _headingLines.total.size.y + 2f;
// create a panel to hold the scrollable panel, scroll bar, and totals
_dataPanel = AddUIComponent<UIPanel>();
if (_dataPanel == null)
{
LogUtil.LogError($"Unable to create data panel.");
return;
}
_dataPanel.name = "DataPanel";
_dataPanel.autoSize = false;
_dataPanel.size = new Vector2(headingPanel.size.x + ScrollbarWidth, height - HeadingTop - headingPanel.size.y - PaddingHeight);
_dataPanel.relativePosition = new Vector3(headingPanel.relativePosition.x, headingPanel.relativePosition.y + headingPanel.size.y);
_dataPanel.anchor = UIAnchorStyle.Left | UIAnchorStyle.Top | UIAnchorStyle.Right;
// create scrollable panel to hold data rows panel
// panel will be scrollable only when Age is selected for row
// for other than Age, dataPanel will be sized so scrolling is not needed
if (!CreateDataScrollablePanel(_dataPanel, out _dataScrollablePanel)) { return; }
// create panel to hold the data rows
_dataRowsPanel = _dataScrollablePanel.AddUIComponent<UIPanel>();
if (_dataRowsPanel == null)
{
LogUtil.LogError($"Unable to create data rows panel.");
return;
}
_dataRowsPanel.name = "DataRowsPanel";
_dataRowsPanel.autoSize = false;
_dataRowsPanel.size = new Vector2(headingPanel.size.x, MaxRows * TextHeight);
_dataRowsPanel.relativePosition = new Vector3(headingPanel.relativePosition.x, headingPanel.relativePosition.y + headingPanel.size.y);
_dataRowsPanel.anchor = UIAnchorStyle.Left | UIAnchorStyle.Top | UIAnchorStyle.Right | UIAnchorStyle.Bottom;
// create the data row UIs
_dataRows = new DataRowUI[MaxRows];
for (int r = 0; r < _dataRows.Length; r++)
{
if (!CreateDataRowUI(_dataRowsPanel, textFont, r * TextHeight, "DataRow" + r, out _dataRows[r])) { return; }
_dataRows[r].description.text = r.ToString();
}
// create panel to hold totals
UIPanel totalPanel = _dataPanel.AddUIComponent<UIPanel>();
if (totalPanel == null)
{
LogUtil.LogError($"Unable to create total panel.");
return;
}
totalPanel.name = "TotalPanel";
totalPanel.autoSize = false;
totalPanel.size = new Vector2(_dataPanel.size.x, HeightOfTotals);
totalPanel.relativePosition = new Vector3(0f, _dataPanel.size.y - HeightOfTotals - SpaceAfterTotalsSection - HeightOfLegend);
totalPanel.anchor = UIAnchorStyle.Left | UIAnchorStyle.Bottom | UIAnchorStyle.Right;
// create lines above the totals
float totalTop = 0;
if (!CreateLines(totalPanel, totalTop, "TotalLine", out _totalLines)) { return; }
// create total row UI
totalTop += 4f;
if (!CreateDataRowUI(totalPanel, textFont, totalTop, "Total", out _totalRow)) { return; }
_totalRow.description.text = "Total";
_totalRow.amountBar.isVisible = false;
// create moving in row UI
totalTop += TextHeight + 12f;
if (!CreateDataRowUI(totalPanel, textFont, totalTop, "MovingIn", out _movingInRow)) { return; }
_movingInRow.description.text = "Moving In";
_movingInRow.amountBar.isVisible = false;
// create deceased row UI
totalTop += TextHeight;
if (!CreateDataRowUI(totalPanel, textFont, totalTop, "Deceased", out _deceasedRow)) { return; }
_deceasedRow.description.text = "Deceased";
_deceasedRow.amountBar.isVisible = false;
// hide duplicates for moving in and deceased, this leaves room for display options
_movingInRow.movingIn.isVisible = false;
_movingInRow.deceased.isVisible = false;
_deceasedRow.movingIn.isVisible = false;
_deceasedRow.deceased.isVisible = false;
// create display option panels
if (!CreateDisplayOptionPanel(totalPanel, textFont, _movingInRow.relativePosition.y, "CountOption", "Count", out _countPanel, out _countCheckBox )) { return; }
if (!CreateDisplayOptionPanel(totalPanel, textFont, _deceasedRow.relativePosition.y, "PercentOption", "Percent", out _percentPanel, out _percentCheckBox)) { return; }
// set initial count or percent from config
SetCheckBox(config.CountStatus ? _countCheckBox : _percentCheckBox, true);
// create legend panel
if (!CreateLegendPanel(textFont)) { return; }
// initialize final demographic buffers
InitializeTempBuffersCounters();
GetCitizenDemographicData((uint)CitizenManager.instance.m_citizens.m_buffer.Length);
_finalCitizens = _tempCitizens;
_finalBuildings = _tempBuildings;
InitializeTempBuffersCounters();
// update panel as if new column was selected
ColumnSelectedIndexChanged(columnSelectionListBox, columnSelectionListBox.selectedIndex);
// initialize cursor label font
PopulationDemographicsLoading.cursorLabel.font = textFont;
// set event handlers
closeButton.eventClicked += CloseClicked;
district.eventSelectedDistrictChanged += SelectedDistrictChanged;
_opacitySlider.eventValueChanged += OpacityValueChanged;
rowSelectionListBox .eventSelectedIndexChanged += RowSelectedIndexChanged;
columnSelectionListBox.eventSelectedIndexChanged += ColumnSelectedIndexChanged;
_countPanel.eventClicked += DisplayOption_eventClicked;
_percentPanel.eventClicked += DisplayOption_eventClicked;
eventVisibilityChanged += PanelVisibilityChanged;
// panel is now initialized and ready for simulation ticks
_initialized = true;
}
catch (Exception ex)
{
LogUtil.LogException(ex);
}
}
#region Create UI
/// <summary>
/// initialize row and column selection attributes
/// </summary>
private bool InitializeRowColumnSelections()
{
// compute age group colors as slightly darker than the colors from the Population Info View panel
PopulationInfoViewPanel populationPanel = UIView.library.Get<PopulationInfoViewPanel>(nameof(PopulationInfoViewPanel));
if (populationPanel == null)
{
LogUtil.LogError("Unable to find PopulationInfoViewPanel.");
return false;
}
const float ColorMultiplierAgeGroup = 0.7f;
Color32 colorAgeGroupChild = (Color)populationPanel.m_ChildColor * ColorMultiplierAgeGroup;
Color32 colorAgeGroupTeen = (Color)populationPanel.m_TeenColor * ColorMultiplierAgeGroup;
Color32 colorAgeGroupYoung = (Color)populationPanel.m_YoungColor * ColorMultiplierAgeGroup;
Color32 colorAgeGroupAdult = (Color)populationPanel.m_AdultColor * ColorMultiplierAgeGroup;
Color32 colorAgeGroupSenior = (Color)populationPanel.m_SeniorColor * ColorMultiplierAgeGroup;
// compute education level colors as slightly darker than the colors from the Education Info View panel
EducationInfoViewPanel educationPanel = UIView.library.Get<EducationInfoViewPanel>(nameof(EducationInfoViewPanel));
if (educationPanel == null)
{
LogUtil.LogError("Unable to find EducationInfoViewPanel.");
return false;
}
const float ColorMultiplierEducation = 0.7f;
Color32 colorEducationUneducated = (Color)educationPanel.m_UneducatedColor * ColorMultiplierEducation;
Color32 colorEducationEducated = (Color)educationPanel.m_EducatedColor * ColorMultiplierEducation;
Color32 colorEducationWellEducated = (Color)educationPanel.m_WellEducatedColor * ColorMultiplierEducation;
Color32 colorEducationHighlyEducated = (Color)educationPanel.m_HighlyEducatedColor * ColorMultiplierEducation;
// set employment colors to yellow, green, and blue
Color32 colorEmploymentStudent = new Color32(160, 160, 64, 255);
Color32 colorEmploymentEmployed = new Color32( 64, 192, 64, 255);
Color32 colorEmploymentUnemployed = new Color32( 64, 64, 192, 255);
// set gender colors to blue and red
Color32 colorGenderMale = new Color32( 64, 64, 192, 255);
Color32 colorGenderFemale = new Color32(192, 64, 64, 255);
// compute happiness colors as slightly darker than the colors from the Happiness Info View panel
if (!InfoManager.exists)
{
LogUtil.LogError("InfoManager is not ready.");
return false;
}
InfoProperties.ModeProperties happinessModeProperties = InfoManager.instance.m_properties.m_modeProperties[(int)InfoManager.InfoMode.Happiness];
Color negativeHappinessColor = happinessModeProperties.m_negativeColor;
Color targetHappinessColor = happinessModeProperties.m_targetColor;
const float ColorMultiplierHappiness = 0.8f;
Color32 colorHappinessBad = Color.Lerp(negativeHappinessColor, targetHappinessColor, 0.00f) * ColorMultiplierHappiness;
Color32 colorHappinessPoor = Color.Lerp(negativeHappinessColor, targetHappinessColor, 0.25f) * ColorMultiplierHappiness;
Color32 colorHappinessGood = Color.Lerp(negativeHappinessColor, targetHappinessColor, 0.50f) * ColorMultiplierHappiness;
Color32 colorHappinessExcellent = Color.Lerp(negativeHappinessColor, targetHappinessColor, 0.75f) * ColorMultiplierHappiness;
Color32 colorHappinessSuperb = Color.Lerp(negativeHappinessColor, targetHappinessColor, 1.00f) * ColorMultiplierHappiness;
// compute health colors as slightly darker than the colors from the Health Info View panel
InfoProperties.ModeProperties healthModeProperties = InfoManager.instance.m_properties.m_modeProperties[(int)InfoManager.InfoMode.Health];
Color32 negativeHealthColor = healthModeProperties.m_negativeColor;
Color32 targetHealthColor = healthModeProperties.m_targetColor;
const float ColorMultiplierHealth = 0.8f;
Color32 colorHealthVerySick = Color.Lerp(negativeHealthColor, targetHealthColor, 0.0f) * ColorMultiplierHealth;
Color32 colorHealthSick = Color.Lerp(negativeHealthColor, targetHealthColor, 0.2f) * ColorMultiplierHealth;
Color32 colorHealthPoor = Color.Lerp(negativeHealthColor, targetHealthColor, 0.4f) * ColorMultiplierHealth;
Color32 colorHealthHealthy = Color.Lerp(negativeHealthColor, targetHealthColor, 0.6f) * ColorMultiplierHealth;
Color32 colorHealthVeryHealthy = Color.Lerp(negativeHealthColor, targetHealthColor, 0.8f) * ColorMultiplierHealth;
Color32 colorHealthExcellent = Color.Lerp(negativeHealthColor, targetHealthColor, 1.0f) * ColorMultiplierHealth;
// compute location colors as shades of orange
// Hotel location is not used because only tourists, not citizens, are guests at hotels
Color32 colorLocationBase = new Color32(254, 230, 177, 255);
Color32 colorLocationHome = (Color)colorLocationBase * 0.70f;
Color32 colorLocationWork = (Color)colorLocationBase * 0.65f;
Color32 colorLocationVisiting = (Color)colorLocationBase * 0.60f;
Color32 colorLocationMoving = (Color)colorLocationBase * 0.55f;
// compute residential colors based on neutral color and average of low and high density zone colors (i.e. similar to colors on Levels Info View panel)
if (!ZoneManager.exists)
{
LogUtil.LogError("ZoneManager is not ready.");
return false;
}
Color[] zoneColors = ZoneManager.instance.m_properties.m_zoneColors;
Color color1 = Color.Lerp(zoneColors[(int)ItemClass.Zone.ResidentialLow], zoneColors[(int)ItemClass.Zone.ResidentialHigh], 0.5f);
Color color0 = Color.Lerp(_neutralColor, color1, 0.20f);
const float ColorMultiplierResidential = 0.8f;
Color32 colorResidentialLevel1 = Color.Lerp(color0, color1, 0.00f) * ColorMultiplierResidential;
Color32 colorResidentialLevel2 = Color.Lerp(color0, color1, 0.25f) * ColorMultiplierResidential;
Color32 colorResidentialLevel3 = Color.Lerp(color0, color1, 0.50f) * ColorMultiplierResidential;
Color32 colorResidentialLevel4 = Color.Lerp(color0, color1, 0.75f) * ColorMultiplierResidential;
Color32 colorResidentialLevel5 = Color.Lerp(color0, color1, 1.00f) * ColorMultiplierResidential;
// compute wealth colors as slightly darker than the colors from the Tourism Info View panel
TourismInfoViewPanel tourismPanel = UIView.library.Get<TourismInfoViewPanel>(nameof(TourismInfoViewPanel));
if (tourismPanel == null)
{
LogUtil.LogError("Unable to find TourismInfoViewPanel.");
return false;
}
UIRadialChart wealthChart = tourismPanel.Find<UIRadialChart>("TouristWealthChart");
if (wealthChart == null)
{
LogUtil.LogError("Unable to find TouristWealthChart.");
return false;
}
const float ColorMultiplierWealth = 0.7f;
Color32 colorWealthLow = (Color)wealthChart.GetSlice(0).innerColor * ColorMultiplierWealth;
Color32 colorWealthMedium = (Color)wealthChart.GetSlice(1).innerColor * ColorMultiplierWealth;
Color32 colorWealthHigh = (Color)wealthChart.GetSlice(2).innerColor * ColorMultiplierWealth;
// compute well being colors as same range as health (there is no info view or other UI in the game for well being)
const float ColorMultiplierWellBeing = 0.8f;
Color32 colorWellBeingVerySad = Color.Lerp(negativeHealthColor, targetHealthColor, 0.00f) * ColorMultiplierWellBeing;
Color32 colorWellBeingSad = Color.Lerp(negativeHealthColor, targetHealthColor, 0.25f) * ColorMultiplierWellBeing;
Color32 colorWellBeingSatisfied = Color.Lerp(negativeHealthColor, targetHealthColor, 0.50f) * ColorMultiplierWellBeing;
Color32 colorWellBeingHappy = Color.Lerp(negativeHealthColor, targetHealthColor, 0.75f) * ColorMultiplierWellBeing;
Color32 colorWellBeingVeryHappy = Color.Lerp(negativeHealthColor, targetHealthColor, 1.00f) * ColorMultiplierWellBeing;
// set row selection attributes
// the heading texts and amount bar colors must be defined in the same order as the corresponding Citizen enum
// Hotel location is not used because only tourists, not citizens, are guests at hotels
_rowSelectionAttributes = new RowSelectionAttributes
{
{ RowSelection.Age, new SelectionAttributes("Age", null, null) /* arrays for age get initialized below */ },
{ RowSelection.AgeGroup, new SelectionAttributes("Age Group", new string[] { "Children", "Teens", "Young Adults", "Adults", "Seniors" },
new Color32[] { colorAgeGroupChild, colorAgeGroupTeen, colorAgeGroupYoung, colorAgeGroupAdult, colorAgeGroupSenior }) },
{ RowSelection.Education, new SelectionAttributes("Education", new string[] { "Uneducated", "Educated", "Well Educated", "Highly Educated" },
new Color32[] { colorEducationUneducated, colorEducationEducated, colorEducationWellEducated, colorEducationHighlyEducated }) },
{ RowSelection.Employment, new SelectionAttributes("Employment", new string[] { "Student ", "Employed", "Jobless" },
new Color32[] { colorEmploymentStudent , colorEmploymentEmployed,colorEmploymentUnemployed }) },
{ RowSelection.Gender, new SelectionAttributes("Gender", new string[] { "Male", "Female" },
new Color32[] { colorGenderMale, colorGenderFemale }) },
{ RowSelection.Happiness, new SelectionAttributes("Happiness", new string[] { "Bad", "Poor", "Good", "Excellent", "Superb" },
new Color32[] { colorHappinessBad, colorHappinessPoor, colorHappinessGood, colorHappinessExcellent, colorHappinessSuperb }) },
{ RowSelection.Health, new SelectionAttributes("Health", new string[] { "Very Sick", "Sick", "Poor", "Healthy", "Very Healthy", "Excellent" },
new Color32[] { colorHealthVerySick, colorHealthSick, colorHealthPoor, colorHealthHealthy, colorHealthVeryHealthy, colorHealthExcellent }) },
{ RowSelection.Location, new SelectionAttributes("Location", new string[] { "Home", "Work", "Visiting", "Moving" },
new Color32[] { colorLocationHome, colorLocationWork, colorLocationVisiting, colorLocationMoving }) },
{ RowSelection.Residential, new SelectionAttributes("Residential", new string[] { "Level 1", "Level 2", "Level 3", "Level 4", "Level 5" },
new Color32[] { colorResidentialLevel1, colorResidentialLevel2, colorResidentialLevel3, colorResidentialLevel4, colorResidentialLevel5 }) },
{ RowSelection.Student, new SelectionAttributes("Student", new string[] { "Not a Student", "Elementary", "High School", "University" },
new Color32[] { colorEducationUneducated, colorEducationEducated, colorEducationWellEducated, colorEducationHighlyEducated }) },
{ RowSelection.Wealth, new SelectionAttributes("Wealth", new string[] { "Low", "Medium", "High" },
new Color32[] { colorWealthLow, colorWealthMedium, colorWealthHigh }) },
{ RowSelection.WellBeing, new SelectionAttributes("Well Being", new string[] { "Very Sad", "Sad", "Satisfied", "Happy", "Very Happy" },
new Color32[] { colorWellBeingVerySad, colorWellBeingSad, colorWellBeingSatisfied, colorWellBeingHappy, colorWellBeingVeryHappy }) }
};
// initialize selection attribute arrays for age
string[] ageHeadingTexts = new string[MaxRows];
Color32[] ageAmountBarColors = new Color32[MaxRows];
for (int r = 0; r < MaxRows; r++)
{
// initialize heading text
ageHeadingTexts[r] = r.ToString();
// initialize amount bar color based on color for corresponding age group
switch (Citizen.GetAgeGroup((int)(r / RealAgePerGameAge)))
{
case Citizen.AgeGroup.Child: ageAmountBarColors[r] = colorAgeGroupChild; break;
case Citizen.AgeGroup.Teen: ageAmountBarColors[r] = colorAgeGroupTeen; break;
case Citizen.AgeGroup.Young: ageAmountBarColors[r] = colorAgeGroupYoung; break;
case Citizen.AgeGroup.Adult: ageAmountBarColors[r] = colorAgeGroupAdult; break;
default: ageAmountBarColors[r] = colorAgeGroupSenior; break;
}
}
_rowSelectionAttributes[RowSelection.Age].headingTexts = ageHeadingTexts;
_rowSelectionAttributes[RowSelection.Age].amountBarColors = ageAmountBarColors;
// set column attributes
// the heading texts must be defined in the same order as the corresponding Citizen enum
// Hotel location is not used because only tourists, not citizens, are guests at hotels
_columnSelectionAttributes = new ColumnSelectionAttributes
{
{ ColumnSelection.None, new SelectionAttributes("None", new string[] { /* intentionally empty array for None */ }, null) },
{ ColumnSelection.AgeGroup, new SelectionAttributes("Age Group", new string[] { "Children", "Teens", "YoungAdult", "Adults", "Seniors" }, null) },
{ ColumnSelection.Education, new SelectionAttributes("Education", new string[] { "Uneducated", "Educated", "Well Edu", "Highly Edu" }, null) },
{ ColumnSelection.Employment, new SelectionAttributes("Employment", new string[] { "Student", "Employed", "Jobless" }, null) },
{ ColumnSelection.Gender, new SelectionAttributes("Gender", new string[] { "Male", "Female" }, null) },
{ ColumnSelection.Happiness, new SelectionAttributes("Happiness", new string[] { "Bad", "Poor", "Good", "Excellent", "Superb" }, null) },
{ ColumnSelection.Health, new SelectionAttributes("Health", new string[] { "Very Sick", "Sick", "Poor", "Healthy", "VeryHealthy", "Excellent" }, null) },
{ ColumnSelection.Location, new SelectionAttributes("Location", new string[] { "Home", "Work", "Visiting", "Moving" }, null) },
{ ColumnSelection.Residential, new SelectionAttributes("Residential", new string[] { "Level 1", "Level 2", "Level 3", "Level 4", "Level 5" }, null) },
{ ColumnSelection.Student, new SelectionAttributes("Student", new string[] { "NotStudent", "Elementary","HighSchool","University" }, null) },
{ ColumnSelection.Wealth, new SelectionAttributes("Wealth", new string[] { "Low", "Medium", "High" }, null) },
{ ColumnSelection.WellBeing, new SelectionAttributes("Well Being", new string[] { "Very Sad", "Sad", "Satisfied", "Happy", "VeryHappy" }, null) }
};
// success
return true;
}
/// <summary>
/// create label that goes above selection list box
/// </summary>
private bool CreateSelectionLabel(UIFont textFont, out UILabel selectionLabel)
{
// create the label on the demographics panel
selectionLabel = AddUIComponent<UILabel>();
if (selectionLabel == null)
{
LogUtil.LogError($"Unable to create selection label.");
return false;
}
// set common properties
selectionLabel.font = textFont;
selectionLabel.textScale = TextScale;
selectionLabel.textColor = TextColorNormal;
selectionLabel.autoSize = false;
selectionLabel.size = new Vector2(SelectionWidth, TextHeight);
// success
return true;
}
/// <summary>
/// create selection list box
/// </summary>
private bool CreateSelectionListBox(UIFont textFont, string[] items, out UIListBox selectionListBox)
{
// create the list box on the demographics panel
selectionListBox = AddUIComponent<UIListBox>();
if (selectionListBox == null)
{
LogUtil.LogError($"Unable to create selection list box.");
return false;
}
// set common properties
selectionListBox.font = textFont;
selectionListBox.textScale = TextScale;
selectionListBox.itemTextColor = TextColorNormal;
selectionListBox.normalBgSprite = "OptionsDropboxListbox";
selectionListBox.itemHighlight = "ListItemHighlight";
selectionListBox.itemHeight = 16;
selectionListBox.itemPadding = new RectOffset(4, 0, 2, 2);
selectionListBox.items = items;
selectionListBox.autoSize = false;
selectionListBox.size = new Vector2(SelectionWidth, selectionListBox.itemHeight * items.Length);
// success
return true;
}
/// <summary>
/// create a data row UI
/// </summary>
private bool CreateDataRowUI(UIPanel onPanel, UIFont textFont, float top, string namePrefix, out DataRowUI dataRow)
{
// create new data panel
dataRow = onPanel.AddUIComponent<DataRowUI>();
dataRow.name = namePrefix + "Panel";
dataRow.autoSize = false;
dataRow.size = new Vector2(onPanel.size.x, TextHeight);
dataRow.relativePosition = new Vector3(0f, top);
dataRow.anchor = UIAnchorStyle.Left | UIAnchorStyle.Top | UIAnchorStyle.Right;
// create label for description
dataRow.description = dataRow.AddUIComponent<UILabel>();
if (dataRow.description == null)
{
LogUtil.LogError($"Unable to create description label for [{namePrefix}].");
return false;
}
dataRow.description.name = namePrefix + "Description";