public void SetReadout(EncounterState state, EncounterZone zone, bool hasIntel) { GetNode <Label>("ReadoutContainer/NameEncounterBar/ZoneNameLabel").Text = zone.ZoneName; if (hasIntel) { GetNode <Label>("ReadoutContainer/NameEncounterBar/ZoneEncounterLabel").Text = zone.ReadoutEncounterName; } else { GetNode <Label>("ReadoutContainer/NameEncounterBar/ZoneEncounterLabel").Text = "NO INTEL"; } var itemsBar = GetNode <HBoxContainer>("ReadoutContainer/ItemsFeaturesBar"); if (hasIntel) { var itemsTextureSize = itemsBar.RectSize.y; foreach (var itemReadout in zone.ReadoutItems) { if (state.GetEntityById(itemReadout.EntityId) != null) { itemsBar.AddChild(BuildTextureRect(itemReadout.TexturePath, itemsTextureSize)); } else { itemsBar.AddChild(BuildTextureRect("res://resources/checkmark_18x18.png", itemsTextureSize)); } } } else { var noIntelLabel = new Label(); noIntelLabel.Text = "-?-"; itemsBar.AddChild(noIntelLabel); } var featuresBar = GetNode <HBoxContainer>("ReadoutContainer/ItemsFeaturesBar"); if (hasIntel) { var featuresTextureSize = featuresBar.RectSize.y; foreach (var featureReadout in zone.ReadoutFeatures) { if (state.GetEntityById(featureReadout.EntityId) != null) { featuresBar.AddChild(BuildTextureRect(featureReadout.TexturePath, featuresTextureSize)); } else { featuresBar.AddChild(BuildTextureRect("res://resources/checkmark_18x18.png", featuresTextureSize)); } } } else { var noIntelLabel = new Label(); noIntelLabel.Text = "-?-"; featuresBar.AddChild(noIntelLabel); } }
private static bool ResolveMove(MoveAction action, EncounterState state) { Entity actor = state.GetEntityById(action.ActorId); var positionComponent = state.GetEntityById(action.ActorId).GetComponent <PositionComponent>(); var oldPosition = positionComponent.EncounterPosition; if (positionComponent.EncounterPosition == action.TargetPosition) { GD.PrintErr(string.Format("Entity {0}:{1} tried to move to its current position {2}", actor.EntityName, actor.EntityId, action.TargetPosition)); return(false); } else { state.TeleportEntity(actor, action.TargetPosition, ignoreCollision: false); var unitComponent = actor.GetComponent <UnitComponent>(); if (unitComponent != null) { state.GetUnit(unitComponent.UnitId).NotifyEntityMoved(oldPosition, action.TargetPosition); } // If you go into the retreat zone you insta-die if (IsInRetreatZone(state, action.TargetPosition)) { if (actor.GetComponent <PlayerComponent>() != null) { state.NotifyPlayerRetreat(); return(false); } else { ResolveAction(new DestroyAction(action.ActorId), state); } } return(true); } }
private static bool ResolveFireProjectile(FireProjectileAction action, EncounterState state) { var actorPosition = state.GetEntityById(action.ActorId).GetComponent <PositionComponent>().EncounterPosition; Entity projectile = EntityBuilder.CreateProjectileEntity( state.GetEntityById(action.ActorId), action.ProjectileType, action.Power, action.PathFunction(actorPosition), action.Speed, state.CurrentTick ); state.PlaceEntity(projectile, actorPosition, true); return(true); }
private static bool ResolveDestroy(DestroyAction action, EncounterState state) { Entity entity = state.GetEntityById(action.ActorId); var onDeathComponent = entity.GetComponent <OnDeathComponent>(); // this 'shouldRemoveEntity' code is slightly confusing, simplify it if you come back to it bool shouldRemoveEntity = true; if (onDeathComponent != null) { foreach (var effectType in onDeathComponent.ActiveEffectTypes) { var effectStopsRemoval = !ResolveOnDeathEffect(effectType, state); if (effectStopsRemoval) { shouldRemoveEntity = false; } } } if (shouldRemoveEntity) { state.RemoveEntity(entity); return(true); } else { return(false); } }
private static bool ResolveRangedAttack(RangedAttackAction action, EncounterState state) { Entity attacker = state.GetEntityById(action.ActorId); Entity defender = action.TargetEntity; var attackerComponent = attacker.GetComponent <AttackerComponent>(); var defenderComponent = defender.GetComponent <DefenderComponent>(); if (defenderComponent.IsInvincible) { var logMessage = string.Format("[b]{0}[/b] hits [b]{1}[/b], but the attack has no effect!", attacker.EntityName, defender.EntityName); LogAttack(attacker, defender, defenderComponent, logMessage, state); } else { // We don't allow underflow damage, though that could be a pretty comical mechanic... int damage = Math.Max(0, attackerComponent.Power - defenderComponent.Defense); defenderComponent.RemoveHp(damage); if (defenderComponent.CurrentHp <= 0) { var logMessage = string.Format("[b]{0}[/b] hits [b]{1}[/b] for {2} damage, destroying it!", attacker.EntityName, defender.EntityName, damage); // Assign XP to the entity that fired the projectile var projectileSource = state.GetEntityById(attackerComponent.SourceEntityId); var xpValueComponent = defender.GetComponent <XPValueComponent>(); if (projectileSource != null && xpValueComponent != null && projectileSource.GetComponent <XPTrackerComponent>() != null) { projectileSource.GetComponent <XPTrackerComponent>().AddXP(xpValueComponent.XPValue, attackerComponent, defenderComponent, state); logMessage += String.Format(" [b]{0}[/b] gains {1} XP!", projectileSource.EntityName, xpValueComponent.XPValue); } LogAttack(attacker, defender, defenderComponent, logMessage, state); ResolveAction(new DestroyAction(defender.EntityId), state); } else { var logMessage = string.Format("[b]{0}[/b] hits [b]{1}[/b] for {2} damage!", attacker.EntityName, defender.EntityName, damage); LogAttack(attacker, defender, defenderComponent, logMessage, state); } } return(true); }
// Currently, each use effect is its own component. If we run into a case where we have too many effects, we can push the // effects into the usable component itself, similarly to status effects (though status effects are their own mess right now) // which would probably be better for building on. private static bool ResolveUse(UseAction action, EncounterState state) { var user = state.GetEntityById(action.ActorId); // This is another issue that'd be solved with a global Entity lookup - though not the removal part. Entity usable = null; if (action.FromInventory) { var userInventory = user.GetComponent <InventoryComponent>(); usable = userInventory.StoredEntityById(action.UsableId); if (usable.GetComponent <UsableComponent>() == null) { state.LogMessage(string.Format("{0} is not usable!", usable.EntityName), failed: true); return(false); } else { userInventory.RemoveEntity(usable); } } else { usable = state.GetEntityById(action.UsableId); if (usable.GetComponent <UsableComponent>() == null) { state.LogMessage(string.Format("{0} is not usable!", usable.EntityName), failed: true); return(false); } else { state.RemoveEntity(usable); } } state.LogMessage(string.Format("{0} used {1}!", user.EntityName, usable.EntityName)); ResolveUseEffects(user, usable, state); // We assume all items are single-use; this will change if I deviate from the reference implementation! usable.QueueFree(); return(true); }
private static bool ResolveWait(WaitAction action, EncounterState state) { var defenderComponent = state.GetEntityById(action.ActorId).GetComponent <DefenderComponent>(); if (defenderComponent != null) { defenderComponent.RestoreFooting(); } return(true); }
private static bool ResolveOnDeathEffect(DestroyAction action, string effectType, EncounterState state) { if (effectType == OnDeathEffectType.PLAYER_VICTORY) { state.NotifyPlayerVictory(); return(true); } else if (effectType == OnDeathEffectType.PLAYER_DEFEAT) { state.NotifyPlayerDefeat(); return(true); } else if (effectType == OnDeathEffectType.REMOVE_FROM_UNIT) { var unit = state.GetUnit(state.GetEntityById(action.ActorId).GetComponent <UnitComponent>().UnitId); unit.NotifyEntityDestroyed(state.GetEntityById(action.ActorId)); return(false); } else { throw new NotImplementedException(String.Format("Don't know how to resolve on death effect type {0}", effectType)); } }
public List <EncounterAction> DecideNextAction(EncounterState state, Entity parent) { if (Path.AtEnd) { var actions = new List <EncounterAction>(); var target = state.GetEntityById(this.TargetId); if (target != null) { actions.Add(new RangedAttackAction(parent.EntityId, state.GetEntityById(this.TargetId))); } actions.Add(new DestroyAction(parent.EntityId)); return(actions);; } else { var nextPosition = Path.Step(); return(new List <EncounterAction>() { new MoveAction(parent.EntityId, nextPosition) }); } }
private static bool ResolveMove(MoveAction action, EncounterState state) { Entity actor = state.GetEntityById(action.ActorId); var positionComponent = state.GetEntityById(action.ActorId).GetComponent <PositionComponent>(); if (positionComponent.EncounterPosition == action.TargetPosition) { GD.PrintErr(string.Format("Entity {0}:{1} tried to move to its current position {2}", actor.EntityName, actor.EntityId, action.TargetPosition)); return(false); } else if (state.IsPositionBlocked(action.TargetPosition)) { var blocker = state.BlockingEntityAtPosition(action.TargetPosition.X, action.TargetPosition.Y); var actorCollision = actor.GetComponent <CollisionComponent>(); if (actorCollision.OnCollisionAttack) { Attack(actor, blocker, state); } if (actorCollision.OnCollisionSelfDestruct) { state.TeleportEntity(actor, action.TargetPosition, ignoreCollision: true); if (state.FoVCache.IsVisible(action.TargetPosition)) { positionComponent.PlayExplosion(); } ResolveAction(new DestroyAction(action.ActorId), state); } return(true); } else { state.TeleportEntity(actor, action.TargetPosition, ignoreCollision: false); return(true); } }
public static bool ResolveEndTurn(string entityId, EncounterState state) { Entity entity = state.GetEntityById(entityId); if (entity != null) { var actionTimeComponent = entity.GetComponent <ActionTimeComponent>(); actionTimeComponent.EndTurn(entity.GetComponent <SpeedComponent>(), entity.GetComponent <StatusEffectTrackerComponent>()); state.EntityHasEndedTurn(entity); return(true); } else { return(false); } }
private static bool ResolveUseStairs(UseStairsAction action, EncounterState state) { var actorPosition = state.GetEntityById(action.ActorId).GetComponent <PositionComponent>().EncounterPosition; var stairs = state.EntitiesAtPosition(actorPosition.X, actorPosition.Y) .FirstOrDefault(e => e.GetComponent <StairsComponent>() != null); if (stairs != null) { state.ResetStateForNewLevel(state.Player, state.DungeonLevel + 1); state.WriteToFile(); return(true); } else { state.LogMessage("No jump point found!", failed: true); return(false); } }
private static bool ResolveGetItem(GetItemAction action, EncounterState state) { var actor = state.GetEntityById(action.ActorId); var inventoryComponent = actor.GetComponent <InventoryComponent>(); var actorPosition = actor.GetComponent <PositionComponent>().EncounterPosition; var item = state.EntitiesAtPosition(actorPosition.X, actorPosition.Y) .FirstOrDefault(e => e.GetComponent <StorableComponent>() != null); if (item == null) { state.LogMessage("No item found!", failed: true); return(false); } else if (item.GetComponent <UsableComponent>() != null && item.GetComponent <UsableComponent>().UseOnGet) { // The responsibility for removing/not removing the usable from the EncounterState is in the usage code. bool successfulUsage = ResolveUse(new UseAction(actor.EntityId, item.EntityId, false), state); if (!successfulUsage) { GD.PrintErr(string.Format("Item {0} was not successfully used after being picked up!", item.EntityName)); } return(true); } else if (!inventoryComponent.CanFit(item)) { state.LogMessage(string.Format("[b]{0}[/b] can't fit the [b]{1}[/b] in its inventory!", actor.EntityName, item.EntityName), failed: true); return(false); } else { state.RemoveEntity(item); actor.GetComponent <InventoryComponent>().AddEntity(item); var logMessage = string.Format("[b]{0}[/b] has taken the [b]{1}[/b]", actor.EntityName, item.EntityName); state.LogMessage(logMessage); return(true); } }
/** * A zone is considered "explored" when: * 1: All storables have been found * 1a: If your inventory is not full, all storable items have been picked up * 2: All non-storable features have been seen * * Actual % of FoW revealed is not important. This is kinda cheaty, because it implies the autopilot knows intel for the * area, but also, who cares? It's fine. Likewise it doesn't factor into account having destroyed the enemies because if * there is an encounter, you'll definitely run into it! */ private static bool ResolveAutopilotContinueExplore(EncounterState state) { var player = state.Player; var playerComponent = player.GetComponent <PlayerComponent>(); var playerPos = player.GetComponent <PositionComponent>().EncounterPosition; var path = playerComponent.AutopilotPath; var zone = state.GetZoneById(playerComponent.AutopilotZoneId); var nextUngottenStorable = zone.ReadoutItems.Concat(zone.ReadoutFeatures) .FirstOrDefault(i => state.GetEntityById(i.EntityId) != null && state.GetEntityById(i.EntityId).GetComponent <StorableComponent>() != null); // We rely on the fact that there's no ungettable features other than stairs, and that there's only one stair, to make this // work. If there were multiple ungettable features you'd want to put them into a list and autopilot between them so you // could cycle the 'x' button to find the one you want. var stairs = zone.ReadoutFeatures.FirstOrDefault(r => state.GetEntityById(r.EntityId).GetComponent <StairsComponent>() != null && state.GetEntityById(r.EntityId).GetComponent <PositionComponent>().EncounterPosition != playerPos ); if (PlayerSeesEnemies(state)) { ResolveAction(new AutopilotEndAction(player.EntityId, AutopilotEndReason.ENEMY_DETECTED), state); return(false); } // If you are already on a path, progress on the path else if (path != null) { if (path.AtEnd) { playerComponent.ClearAutopilotPath(); return(false); } else { Rulebook.ResolveAction(new MoveAction(player.EntityId, path.Step()), state); return(true); } } // If you're on top of a storable item, get it else if (state.EntitiesAtPosition(playerPos.X, playerPos.Y).Any(e => e.GetComponent <StorableComponent>() != null)) { var nextStorable = state.EntitiesAtPosition(playerPos.X, playerPos.Y).First(e => e.GetComponent <StorableComponent>() != null); if (ResolveAction(new GetItemAction(player.EntityId), state)) { return(true); } else { ResolveAction(new AutopilotEndAction(player.EntityId, AutopilotEndReason.INVENTORY_FULL), state); return(false); } } // If there are any storable items, move towards them else if (nextUngottenStorable != null) { var nextPos = state.GetEntityById(nextUngottenStorable.EntityId).GetComponent <PositionComponent>().EncounterPosition; var foundPath = Pathfinder.AStarWithNewGrid(playerPos, nextPos, state, 900); if (foundPath == null) { ResolveAction(new AutopilotEndAction(player.EntityId, AutopilotEndReason.NO_PATH), state); return(false); } else { foundPath.Add(nextPos); playerComponent.LayInAutopilotPathForExploration(new EncounterPath(foundPath)); return(true); } } // If this is the stairs zone, go to the stairs else if (stairs != null) { var nextPos = state.GetEntityById(stairs.EntityId).GetComponent <PositionComponent>().EncounterPosition; var foundPath = Pathfinder.AStarWithNewGrid(playerPos, nextPos, state, 900); if (foundPath == null) { ResolveAction(new AutopilotEndAction(player.EntityId, AutopilotEndReason.NO_PATH), state); return(false); } else { foundPath.Add(nextPos); playerComponent.LayInAutopilotPathForExploration(new EncounterPath(foundPath)); return(true); } } // Otherwise you're done! else { ResolveAction(new AutopilotEndAction(player.EntityId, AutopilotEndReason.TASK_COMPLETED), state); return(false); } }
private static bool ResolveMeleeAttack(MeleeAttackAction action, EncounterState state) { Entity attacker = state.GetEntityById(action.ActorId); Entity defender = action.TargetEntity; var attackerComponent = attacker.GetComponent <AttackerComponent>(); var attackerDefenderComponent = attacker.GetComponent <DefenderComponent>(); var defenderComponent = defender.GetComponent <DefenderComponent>(); if (defenderComponent.IsInvincible) { var logMessage = string.Format("[b]{0}[/b] hits [b]{1}[/b], but the attack has no effect!", attacker.EntityName, defender.EntityName); LogAttack(attacker, defender, defenderComponent, logMessage, state); } else { bool isPlayer = attacker == state.Player; bool hit = false; bool killed = false; var attackReport = AttackHits(state.EncounterRand, attackerComponent.MeleeAttack, attackerDefenderComponent.FootingPenalty, defenderComponent.MeleeDefense, defenderComponent.FootingPenalty); attackerDefenderComponent.NotifyParentHasAttacked(); if (!attackReport.Item3) { var logMessage = string.Format("[b]{0}[/b] attacks [b]{1}[/b], but misses! ({2}% chance to hit)", attacker.EntityName, defender.EntityName, attackReport.Item1); LogAttack(attacker, defender, defenderComponent, logMessage, state); return(true); } // We don't allow underflow damage, though that could be a pretty comical mechanic... int weaponDamage = Math.Max(0, attackerComponent.Power - defenderComponent.Defense); int shieldedByFooting = (int)Math.Floor(weaponDamage * defenderComponent.PercentageFooting); int hpDamage = weaponDamage - shieldedByFooting; int footingDamage = shieldedByFooting * 3; hit = true; defenderComponent.RemoveHp(hpDamage); defenderComponent.RemoveFooting(footingDamage); if (defenderComponent.CurrentHp <= 0) { killed = true; var logMessage = string.Format("[b]{0}[/b] hits [b]{1}[/b] for {2} damage, killing it! ({3}% chance to hit)", attacker.EntityName, defender.EntityName, hpDamage, attackReport.Item1); // Assign XP to the entity that fired the projectile var attackerId = state.GetEntityById(attackerComponent.SourceEntityId); var xpValueComponent = defender.GetComponent <XPValueComponent>(); if (attackerId != null && xpValueComponent != null && attackerId.GetComponent <XPTrackerComponent>() != null) { attackerId.GetComponent <XPTrackerComponent>().AddXP(xpValueComponent.XPValue, attackerComponent, defenderComponent, state); logMessage += String.Format(" [b]{0}[/b] gains {1} XP!", attackerId.EntityName, xpValueComponent.XPValue); } LogAttack(attacker, defender, defenderComponent, logMessage, state); ResolveAction(new DestroyAction(defender.EntityId), state); } else { var logMessage = string.Format("[b]{0}[/b] hits [b]{1}[/b] for {2} HP damage and {3} footing damage! ({4}% chance to hit)", attacker.EntityName, defender.EntityName, hpDamage, shieldedByFooting, attackReport.Item1); LogAttack(attacker, defender, defenderComponent, logMessage, state); } // Finally, assign player prestige if (isPlayer && killed) { var logMessage = string.Format("Your allies witness you slaying the [b]{0}[/b]. [b]You gain 5 prestige![/b]", defender.EntityName); attacker.GetComponent <PlayerComponent>().AddPrestige(5, state, logMessage, PrestigeSource.DEFEATING_FOES); } else if (isPlayer && hit) { var logMessage = string.Format("Your allies will remember that injured the [b]{0}[/b]. [b]You gain 1 prestige![/b]", defender.EntityName); attacker.GetComponent <PlayerComponent>().AddPrestige(1, state, logMessage, PrestigeSource.LANDING_HITS); } } return(true); }
public List <TriggeredOrder> ExecuteOrder(EncounterState state) { var unit = state.GetUnit(this.UnitId); if (this.OrderType == OrderType.ADVANCE) { unit.StandingOrder = UnitOrder.ADVANCE; return(null); } else if (this.OrderType == OrderType.OPEN_MANIPULE) { var firstUnit = state.GetEntityById(unit.EntityIdInForPositionZero); unit.UnitFormation = FormationType.MANIPULE_OPENED; unit.StandingOrder = UnitOrder.REFORM; // TODO: dumb hack to make blocks line up var firstUnitPos = firstUnit.GetComponent <PositionComponent>().EncounterPosition; if (unit.UnitFacing == FormationFacing.SOUTH) { unit.RallyPoint = new EncounterPosition(firstUnitPos.X + 1, firstUnitPos.Y); } else if (unit.UnitFacing == FormationFacing.WEST) { unit.RallyPoint = new EncounterPosition(firstUnitPos.X, firstUnitPos.Y + 1); } else { unit.RallyPoint = firstUnitPos; } return(null); } else if (this.OrderType == OrderType.ROUT) { unit.StandingOrder = UnitOrder.ROUT; return(null); } else if (this.OrderType == OrderType.DECLARE_VICTORY) { state.NotifyArmyVictory(); return(null); } else if (this.OrderType == OrderType.DECLARE_DEFEAT) { state.NotifyArmyDefeat(); return(null); } else if (this.OrderType == OrderType.PRINT) { GD.Print("!!!!!!! PRINT ORDER EXECUTED !!!!!!!!!"); return(null); } else if (this.OrderType == OrderType.PREPARE_SWEEP_NEXT_LANE) { return(OrderFns.ExecutePREPARE_SWEEP_NEXT_LANE(state, unit)); } else if (this.OrderType == OrderType.ROTATE_AND_REFORM_AT) { if (this.NewPosition == null) { unit.RallyPoint = unit.AveragePosition; } else { unit.RallyPoint = this.NewPosition.Value; } unit.UnitFacing = this.NewFacing; unit.StandingOrder = UnitOrder.REFORM; return(null); } else { throw new NotImplementedException("lol: " + this.OrderType); } }
public void RefreshStats(EncounterState state) { var player = state.Player; // Left column var playerDefenderComponent = player.GetComponent <DefenderComponent>(); var newHPText = string.Format("HP: {0}/{1}", playerDefenderComponent.CurrentHp, playerDefenderComponent.MaxHp); GetNode <Label>("SidebarVBox/StatsAndPositionHBox/StatsBlock/HPLabel").Text = newHPText; var playerComponent = player.GetComponent <PlayerComponent>(); var newAttackPowerText = string.Format("Laser Power: {0}", playerComponent.CuttingLaserPower); GetNode <Label>("SidebarVBox/StatsAndPositionHBox/StatsBlock/AttackPowerLabel").Text = newAttackPowerText; var speedComponent = player.GetComponent <SpeedComponent>(); var newSpeedText = string.Format("Speed: {0}", speedComponent.Speed); GetNode <Label>("SidebarVBox/StatsAndPositionHBox/StatsBlock/SpeedLabel").Text = newSpeedText; var invComponent = player.GetComponent <InventoryComponent>(); var newInvText = string.Format("Cargo Space: {0}/{1}", invComponent.InventoryUsed, invComponent.InventorySize); GetNode <Label>("SidebarVBox/StatsAndPositionHBox/StatsBlock/InventoryLabel").Text = newInvText; var xpComponent = player.GetComponent <XPTrackerComponent>(); var newLevelText = string.Format("Level: {0}", xpComponent.Level); GetNode <Label>("SidebarVBox/StatsAndPositionHBox/StatsBlock/LevelLabel").Text = newLevelText; var newXPText = string.Format("Experience: {0}/{1}", xpComponent.XP, xpComponent.NextLevelAtXP); GetNode <Label>("SidebarVBox/StatsAndPositionHBox/StatsBlock/ExperienceLabel").Text = newXPText; // Right column var playerPos = player.GetComponent <PositionComponent>().EncounterPosition; var newTurnReadoutText = string.Format("Turn: {0:0.00}", state.CurrentTick / 100); GetNode <Label>("SidebarVBox/StatsAndPositionHBox/PositionBlock/TurnReadoutLabel").Text = newTurnReadoutText; var newSectorZoneText = string.Format("Current Sector: {0}", state.DungeonLevel); GetNode <Label>("SidebarVBox/StatsAndPositionHBox/PositionBlock/SectorLabel").Text = newSectorZoneText; string newPositionZoneText = string.Format("Point: ({0}, {1})", playerPos.X, playerPos.Y); GetNode <Label>("SidebarVBox/StatsAndPositionHBox/PositionBlock/PositionLabel").Text = newPositionZoneText; var containingZone = state.ContainingZone(playerPos.X, playerPos.Y); if (containingZone == null) { GetNode <Label>("SidebarVBox/StatsAndPositionHBox/PositionBlock/ZoneHeaderLabel").Text = "Not In Zone"; GetNode <VBoxContainer>("SidebarVBox/StatsAndPositionHBox/PositionBlock/ZoneBlock").Hide(); } else { GetNode <Label>("SidebarVBox/StatsAndPositionHBox/PositionBlock/ZoneHeaderLabel").Text = containingZone.ZoneName; GetNode <VBoxContainer>("SidebarVBox/StatsAndPositionHBox/PositionBlock/ZoneBlock").Show(); // ...that's a really long line, am I abusing the layout? GetNode <Label>("SidebarVBox/StatsAndPositionHBox/PositionBlock/ZoneBlock/EnemiesWarningContainer/EnemiesWarningLabel").Text = containingZone.ReadoutEncounterName; // This is very fragile! var itemsBasePath = "SidebarVBox/StatsAndPositionHBox/PositionBlock/ZoneBlock/ItemsFeatureReadout/ZoneItemReadout/ZoneItems/ZoneItem"; var itemReadouts = new List <EntityReadout>(containingZone.ReadoutItems); for (int i = 0; i < 3; i++) { var textureRect = GetNode <TextureRect>(itemsBasePath + (i + 1)); if (itemReadouts.Count >= i + 1) { var readout = itemReadouts[i]; textureRect.Show(); var entity = state.GetEntityById(readout.EntityId); if (entity != null) { textureRect.Texture = GD.Load <Texture>(entity.GetComponent <DisplayComponent>().TexturePath); } else { textureRect.Texture = GD.Load <Texture>("res://resources/checkmark_18x18.png"); } } else { textureRect.Hide(); } } var featuresBasePath = "SidebarVBox/StatsAndPositionHBox/PositionBlock/ZoneBlock/ItemsFeatureReadout/ZoneFeaturesReadout/ZoneFeatures/ZoneFeature"; var featureReadouts = new List <EntityReadout>(containingZone.ReadoutFeatures); for (int i = 0; i < 2; i++) { var textureRect = GetNode <TextureRect>(featuresBasePath + (i + 1)); if (featureReadouts.Count >= i + 1) { var readout = featureReadouts[i]; textureRect.Show(); var entity = state.GetEntityById(readout.EntityId); if (entity != null) { textureRect.Texture = GD.Load <Texture>(entity.GetComponent <DisplayComponent>().TexturePath); } else { textureRect.Texture = GD.Load <Texture>("res://resources/checkmark_18x18.png"); } } else { textureRect.Hide(); } } } }