Compare commits
2 commits
7b82325729
...
4cad719290
| Author | SHA1 | Date | |
|---|---|---|---|
| 4cad719290 | |||
| 1e3b05d5c8 |
26 changed files with 2640 additions and 739 deletions
BIN
assets/Pictures/Room_window.jpg
Normal file
BIN
assets/Pictures/Room_window.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 154 KiB |
|
|
@ -19,17 +19,17 @@ void main()
|
|||
{
|
||||
vec2 uv = fragTexCoord;
|
||||
|
||||
// 1. Digital Glitch / Chromatic Aberration
|
||||
// 1. Digital Glitch / Tactical Chromatic Aberration
|
||||
float glitch = clamp(u_glitchIntensity, 0.0, 1.0);
|
||||
if (glitch > 0.001)
|
||||
{
|
||||
// Horizontal line slice displacement
|
||||
float slice = step(0.65, sin(uv.y * 35.0 + u_time * 25.0));
|
||||
float sliceOffset = (slice * 2.0 - 1.0) * 0.018 * glitch;
|
||||
float slice = step(0.68, sin(uv.y * 42.0 + u_time * 30.0));
|
||||
float sliceOffset = (slice * 2.0 - 1.0) * 0.015 * glitch;
|
||||
uv.x += sliceOffset;
|
||||
|
||||
// Chromatic RGB separation
|
||||
float split = 0.012 * glitch;
|
||||
float split = 0.010 * glitch;
|
||||
float r = texture(texture0, vec2(uv.x + split, uv.y)).r;
|
||||
float g = texture(texture0, uv).g;
|
||||
float b = texture(texture0, vec2(uv.x - split, uv.y)).b;
|
||||
|
|
@ -40,43 +40,34 @@ void main()
|
|||
finalColor = texture(texture0, uv);
|
||||
}
|
||||
|
||||
// 2. Neon Bloom / Emissive Halo
|
||||
vec2 texel = 1.0 / u_resolution;
|
||||
vec3 bloom = vec3(0.0);
|
||||
float bloomWeight = 0.0;
|
||||
|
||||
// 9-tap bright-pass sample
|
||||
vec2 offsets[8] = vec2[](
|
||||
vec2(-2.0, 0.0), vec2( 2.0, 0.0),
|
||||
vec2( 0.0, -2.0), vec2( 0.0, 2.0),
|
||||
vec2(-1.5, -1.5), vec2( 1.5, -1.5),
|
||||
vec2(-1.5, 1.5), vec2( 1.5, 1.5)
|
||||
);
|
||||
|
||||
for (int i = 0; i < 8; ++i)
|
||||
// 2. Tactical Color Grading (High Contrast Monochrome + Vivid Crimson Accent)
|
||||
// Desaturate non-red tones toward tactical gunmetal/monochrome
|
||||
float luma = dot(finalColor.rgb, vec3(0.299, 0.587, 0.114));
|
||||
|
||||
// Check if the color is an intentional tactical accent (Crimson, Cobalt EMP, Amber, Acid, Violet)
|
||||
float maxC = max(finalColor.r, max(finalColor.g, finalColor.b));
|
||||
float minC = min(finalColor.r, min(finalColor.g, finalColor.b));
|
||||
float sat = (maxC > 0.05) ? ((maxC - minC) / maxC) : 0.0;
|
||||
bool isTacticalAccent = (sat > 0.36) || (finalColor.r > 0.35 && finalColor.r > finalColor.g * 1.3);
|
||||
|
||||
if (!isTacticalAccent)
|
||||
{
|
||||
vec3 col = texture(texture0, uv + offsets[i] * texel * 1.8).rgb;
|
||||
float brightness = dot(col, vec3(0.2126, 0.7152, 0.0722));
|
||||
if (brightness > 0.55)
|
||||
{
|
||||
bloom += (col - 0.45) * 1.4;
|
||||
bloomWeight += 1.0;
|
||||
}
|
||||
// Pull low-saturation environmental surfaces toward high-contrast monochrome steel
|
||||
vec3 mono = vec3(luma);
|
||||
finalColor.rgb = mix(finalColor.rgb, mono, 0.80);
|
||||
}
|
||||
|
||||
if (bloomWeight > 0.0)
|
||||
{
|
||||
bloom /= 8.0;
|
||||
finalColor.rgb += bloom * 0.75;
|
||||
}
|
||||
// Enhance contrast (crush deep blacks, crispen bright highlights)
|
||||
finalColor.rgb = (finalColor.rgb - 0.5) * 1.18 + 0.5;
|
||||
finalColor.rgb = clamp(finalColor.rgb, 0.0, 1.0);
|
||||
|
||||
// 3. Vignette (Smooth cinematic edge darkening)
|
||||
// 3. Cinematic Corner Vignette (Tactical Helmet Visor Shadow)
|
||||
float d = distance(uv, vec2(0.5, 0.5));
|
||||
float vignette = smoothstep(0.82, 0.38, d);
|
||||
finalColor.rgb *= mix(0.58, 1.0, vignette);
|
||||
float vignette = smoothstep(0.78, 0.32, d);
|
||||
finalColor.rgb *= mix(0.40, 1.0, vignette);
|
||||
|
||||
// 4. Subtle Cyber Scanlines
|
||||
float scanline = sin(uv.y * u_resolution.y * 1.8) * 0.035;
|
||||
// 4. Subtle Tactical CRT Scanlines
|
||||
float scanline = sin(uv.y * u_resolution.y * 1.5) * 0.025;
|
||||
finalColor.rgb -= scanline;
|
||||
|
||||
finalColor.a = 1.0;
|
||||
|
|
|
|||
|
|
@ -11,11 +11,17 @@ void CombatSystem::startCombat(Player& player, std::unique_ptr<Enemy> enemy) {
|
|||
m_phase = CombatPhase::PLAYER_TURN;
|
||||
m_phaseTimer = 0.0f;
|
||||
m_shieldBoosted = false;
|
||||
m_ghostBonusDmg = false;
|
||||
m_defeatedEnemyWasElite = false;
|
||||
m_tripleCooldown = 0;
|
||||
m_lastFiredAmmo = AmmoType::EMPTY;
|
||||
m_combatLog.clear();
|
||||
m_floatingTexts.clear();
|
||||
|
||||
addLog("--- HOSTILE INTERFACE DETECTED ---");
|
||||
if (m_enemy->isElite()) {
|
||||
addLog(">>> WARNING: ELITE DRONE CLASS DETECTED! <<<");
|
||||
}
|
||||
addLog(m_enemy->getName() + " attacks with [" + getAmmoInfo(m_enemy->getAttackElement()).shortName + "]!");
|
||||
addLog("WEAKNESS: [" + getAmmoInfo(m_enemy->getWeakness()).shortName + "] | Ready your revolver!");
|
||||
}
|
||||
|
|
@ -35,6 +41,26 @@ void CombatSystem::update(float dt, Player& player) {
|
|||
}
|
||||
}
|
||||
|
||||
auto handleVictory = [&](Player& p) {
|
||||
m_phase = CombatPhase::VICTORY;
|
||||
m_defeatedEnemyWasElite = m_enemy->isElite();
|
||||
int chips = 20 + (std::rand() % 16);
|
||||
if (m_enemy->isElite()) chips = static_cast<int>(chips * 2.0f);
|
||||
if (p.hasImplant(ImplantType::SALVAGE_PROTOCOL)) chips = static_cast<int>(chips * 1.5f);
|
||||
p.addTechScraps(chips);
|
||||
|
||||
addLog(">>> TARGET TERMINATED! Protocol cleared <<<");
|
||||
addLog("CHIPS HARVESTED: +" + std::to_string(chips) + " CORE CHIPS");
|
||||
addFloatingText("+" + std::to_string(chips) + " CHIPS", { 640, 140 }, CyberColors::NeonYellow);
|
||||
|
||||
if (p.hasImplant(ImplantType::ADRENALINE_DRUM)) {
|
||||
AmmoType randomAmmo = static_cast<AmmoType>(std::rand() % 6);
|
||||
p.addAmmo(randomAmmo, 1);
|
||||
addLog("ADRENALINE DRUM: Loaded +1 emergency [" + getAmmoInfo(randomAmmo).shortName + "] cartridge!");
|
||||
}
|
||||
SoundManager::get().playPickup();
|
||||
};
|
||||
|
||||
if (m_phase == CombatPhase::SPINNING_SHOT) {
|
||||
// Wait until cylinder spin decelerates and lands on a chamber
|
||||
if (!player.getRevolver().isSpinning()) {
|
||||
|
|
@ -55,13 +81,7 @@ void CombatSystem::update(float dt, Player& player) {
|
|||
m_burstShotsFired = 3;
|
||||
} else if (m_burstTimer >= 1.65f) {
|
||||
if (!m_enemy->isAlive()) {
|
||||
m_phase = CombatPhase::VICTORY;
|
||||
int chips = 20 + (std::rand() % 16);
|
||||
player.addTechScraps(chips);
|
||||
addLog(">>> TARGET TERMINATED! Protocol cleared <<<");
|
||||
addLog("CHIPS HARVESTED: +" + std::to_string(chips) + " CORE CHIPS");
|
||||
addFloatingText("+" + std::to_string(chips) + " CHIPS", { 640, 140 }, CyberColors::NeonYellow);
|
||||
SoundManager::get().playPickup();
|
||||
handleVictory(player);
|
||||
} else {
|
||||
m_phase = CombatPhase::ENEMY_TURN;
|
||||
executeEnemyTurn(player);
|
||||
|
|
@ -71,13 +91,7 @@ void CombatSystem::update(float dt, Player& player) {
|
|||
m_phaseTimer += dt;
|
||||
if (m_phaseTimer >= 0.75f) {
|
||||
if (!m_enemy->isAlive()) {
|
||||
m_phase = CombatPhase::VICTORY;
|
||||
int chips = 20 + (std::rand() % 16);
|
||||
player.addTechScraps(chips);
|
||||
addLog(">>> TARGET TERMINATED! Protocol cleared <<<");
|
||||
addLog("CHIPS HARVESTED: +" + std::to_string(chips) + " CORE CHIPS");
|
||||
addFloatingText("+" + std::to_string(chips) + " CHIPS", { 640, 140 }, CyberColors::NeonYellow);
|
||||
SoundManager::get().playPickup();
|
||||
handleVictory(player);
|
||||
} else {
|
||||
m_phase = CombatPhase::ENEMY_TURN;
|
||||
executeEnemyTurn(player);
|
||||
|
|
@ -87,20 +101,51 @@ void CombatSystem::update(float dt, Player& player) {
|
|||
m_phaseTimer += dt;
|
||||
if (m_phaseTimer >= 0.85f) {
|
||||
if (!player.isAlive()) {
|
||||
m_phase = CombatPhase::DEFEAT;
|
||||
addLog("FLATLINE! System destroyed.");
|
||||
SoundManager::get().playDamage();
|
||||
} else {
|
||||
m_phase = CombatPhase::PLAYER_TURN;
|
||||
if (m_tripleCooldown > 0) {
|
||||
--m_tripleCooldown;
|
||||
if (player.hasImplant(ImplantType::AUTO_DEFIBRILLATOR) && !player.isDefibrillatorUsed()) {
|
||||
player.setDefibrillatorUsed(true);
|
||||
player.heal(1 - player.getHp());
|
||||
player.restoreShield(50);
|
||||
m_enemy->applyStatus(StatusEffect::SHOCKED, 1, 0);
|
||||
addLog(">>> AUTO-DEFIBRILLATOR DISCHARGED! Lethal shock absorbed, 50 Shield restored! <<<");
|
||||
addFloatingText("DEFIB RESCUE!", { 300, 460 }, CyberColors::NeonGreen);
|
||||
SoundManager::get().playSkillGreat();
|
||||
} else {
|
||||
m_phase = CombatPhase::DEFEAT;
|
||||
addLog("FLATLINE! System destroyed.");
|
||||
SoundManager::get().playDamage();
|
||||
return;
|
||||
}
|
||||
player.updateTurnStart(m_combatLog);
|
||||
if (!player.isAlive()) {
|
||||
}
|
||||
|
||||
m_phase = CombatPhase::PLAYER_TURN;
|
||||
if (m_tripleCooldown > 0) {
|
||||
--m_tripleCooldown;
|
||||
}
|
||||
player.updateTurnStart(m_combatLog);
|
||||
|
||||
if (!player.isAlive()) {
|
||||
if (player.hasImplant(ImplantType::AUTO_DEFIBRILLATOR) && !player.isDefibrillatorUsed()) {
|
||||
player.setDefibrillatorUsed(true);
|
||||
player.heal(1 - player.getHp());
|
||||
player.restoreShield(50);
|
||||
m_enemy->applyStatus(StatusEffect::SHOCKED, 1, 0);
|
||||
addLog(">>> AUTO-DEFIBRILLATOR DISCHARGED! Status damage averted, 50 Shield restored! <<<");
|
||||
addFloatingText("DEFIB RESCUE!", { 300, 460 }, CyberColors::NeonGreen);
|
||||
SoundManager::get().playSkillGreat();
|
||||
} else {
|
||||
m_phase = CombatPhase::DEFEAT;
|
||||
addLog("FLATLINE! System collapsed from status damage.");
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// NANITE WEAVER perk
|
||||
if (player.hasImplant(ImplantType::NANITE_WEAVER) && !m_enemy->getStatuses().empty()) {
|
||||
player.heal(5);
|
||||
player.restoreShield(8);
|
||||
addLog("NANITE WEAVER: Status siphon (+5 HP, +8 Shield).");
|
||||
addFloatingText("+5 HP / +8 SHIELD", { 300, 500 }, CyberColors::NeonGreen);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -123,7 +168,7 @@ void CombatSystem::actionTripleShot(Player& player) {
|
|||
m_phase = CombatPhase::TRIPLE_SHOT_BURST;
|
||||
m_burstTimer = 0.0f;
|
||||
m_burstShotsFired = 0;
|
||||
m_tripleCooldown = 3;
|
||||
m_tripleCooldown = player.hasImplant(ImplantType::HEAVY_HAMMER) ? 2 : 3;
|
||||
}
|
||||
|
||||
void CombatSystem::applyAmmoStatus(AmmoType chamberAmmo, int dealt, Player& player) {
|
||||
|
|
@ -132,9 +177,17 @@ void CombatSystem::applyAmmoStatus(AmmoType chamberAmmo, int dealt, Player& play
|
|||
|
||||
switch (chamberAmmo) {
|
||||
case AmmoType::EMP: {
|
||||
int duration = (tier >= 3) ? 2 : 1;
|
||||
if (player.hasImplant(ImplantType::CONDUCTIVE_SALVO)) {
|
||||
duration += 1;
|
||||
int pierceDmg = m_enemy->takeDamage(15, AmmoType::EMP);
|
||||
addLog("CONDUCTIVE SALVO: EMP pierced shield for " + std::to_string(pierceDmg) + " core damage!");
|
||||
addFloatingText("-" + std::to_string(pierceDmg) + " PIERCE", { 640, 200 }, CyberColors::NeonCyan);
|
||||
}
|
||||
|
||||
float chance = (tier >= 3) ? 1.0f : ((tier == 2) ? 0.85f : info.effectChance);
|
||||
if ((std::rand() % 100) < static_cast<int>(chance * 100)) {
|
||||
m_enemy->applyStatus(StatusEffect::SHOCKED, (tier >= 3 ? 2 : 1), 0);
|
||||
m_enemy->applyStatus(StatusEffect::SHOCKED, duration, 0);
|
||||
addLog(m_enemy->getName() + " is SHOCKED! Reboot delayed.");
|
||||
addFloatingText((tier >= 3) ? "EMP OVERLOAD!" : "STUNNED!", { 640, 180 }, CyberColors::NeonCyan);
|
||||
}
|
||||
|
|
@ -171,6 +224,19 @@ void CombatSystem::applyAmmoStatus(AmmoType chamberAmmo, int dealt, Player& play
|
|||
case AmmoType::NANITE: {
|
||||
float ratio = (tier >= 3) ? 0.85f : ((tier == 2) ? 0.70f : 0.50f);
|
||||
int siphon = std::max(1, static_cast<int>(dealt * ratio));
|
||||
|
||||
if (player.hasImplant(ImplantType::CORROSIVE_LEECH)) {
|
||||
bool isCorroded = false;
|
||||
for (const auto& st : m_enemy->getStatuses()) {
|
||||
if (st.effect == StatusEffect::CORRODED) isCorroded = true;
|
||||
}
|
||||
if (isCorroded) {
|
||||
siphon *= 2;
|
||||
addLog("CORROSIVE LEECH: Nanite swarm siphoned 2x from corroded armor!");
|
||||
addFloatingText("+2X VAMP", { 300, 450 }, CyberColors::NeonGreen);
|
||||
}
|
||||
}
|
||||
|
||||
player.heal(siphon);
|
||||
player.restoreShield(tier >= 3 ? siphon : siphon / 2);
|
||||
addLog("Nanites siphoned +" + std::to_string(siphon) + " HP!");
|
||||
|
|
@ -184,6 +250,7 @@ void CombatSystem::applyAmmoStatus(AmmoType chamberAmmo, int dealt, Player& play
|
|||
|
||||
void CombatSystem::resolveBurstShot(Player& player, int shotNum) {
|
||||
AmmoType chamberAmmo = player.getRevolver().fire();
|
||||
m_lastFiredAmmo = chamberAmmo;
|
||||
const auto& info = getAmmoInfo(chamberAmmo);
|
||||
|
||||
float offsetX = (shotNum - 2) * 45.0f;
|
||||
|
|
@ -208,18 +275,38 @@ void CombatSystem::resolveBurstShot(Player& player, int shotNum) {
|
|||
addLog("[SHOT " + std::to_string(shotNum) + "] Hit with [" + info.name + " " + tierData.tierBadge + "] for " + std::to_string(dealt) + " damage!");
|
||||
addFloatingText("-" + std::to_string(dealt), { 640.0f + offsetX, 240.0f }, info.color);
|
||||
|
||||
if (player.hasImplant(ImplantType::CATALYST_MATRIX)) {
|
||||
bool hasDebuff = false;
|
||||
for (const auto& st : m_enemy->getStatuses()) {
|
||||
if (st.effect == StatusEffect::BURNING || st.effect == StatusEffect::CORRODED) hasDebuff = true;
|
||||
}
|
||||
if (hasDebuff) {
|
||||
int catDmg = m_enemy->takeDamage(25, AmmoType::OVERHEAT);
|
||||
addLog("CATALYST MATRIX: Status detonated for +" + std::to_string(catDmg) + " bonus damage!");
|
||||
addFloatingText("+25 CATALYST", { 640.0f + offsetX, 170.0f }, CyberColors::NeonPink);
|
||||
}
|
||||
}
|
||||
|
||||
applyAmmoStatus(chamberAmmo, dealt, player);
|
||||
}
|
||||
}
|
||||
|
||||
void CombatSystem::resolveShotImpact(Player& player) {
|
||||
AmmoType chamberAmmo = player.getRevolver().fire();
|
||||
m_lastFiredAmmo = chamberAmmo;
|
||||
const auto& info = getAmmoInfo(chamberAmmo);
|
||||
|
||||
if (chamberAmmo == AmmoType::EMPTY) {
|
||||
SoundManager::get().playEmptyClick();
|
||||
addLog("*CLICK!* Blank chamber misfire! No shot.");
|
||||
addFloatingText("MISFIRE!", { 640, 240 }, GRAY);
|
||||
|
||||
if (player.hasImplant(ImplantType::GHOST_CHAMBER)) {
|
||||
player.restoreShield(20);
|
||||
m_ghostBonusDmg = true;
|
||||
addLog("GHOST CHAMBER: Ghost shield pulse +20! Next shot deals +50% damage.");
|
||||
addFloatingText("+20 SHIELD", { 300, 480 }, CyberColors::NeonCyan);
|
||||
}
|
||||
} else {
|
||||
SoundManager::get().playGunshot();
|
||||
|
||||
|
|
@ -227,6 +314,15 @@ void CombatSystem::resolveShotImpact(Player& player) {
|
|||
int tier = player.getAmmoTier(chamberAmmo);
|
||||
auto tierData = getAmmoTierData(chamberAmmo, tier);
|
||||
|
||||
if (player.hasImplant(ImplantType::HEAVY_HAMMER)) {
|
||||
dmg = static_cast<int>(dmg * 1.3f);
|
||||
}
|
||||
if (m_ghostBonusDmg) {
|
||||
dmg = static_cast<int>(dmg * 1.5f);
|
||||
m_ghostBonusDmg = false;
|
||||
addLog("GHOST SURGE: +50% damage applied!");
|
||||
}
|
||||
|
||||
bool isWeakness = (chamberAmmo == m_enemy->getWeakness());
|
||||
if (isWeakness) {
|
||||
addLog("WEAKNESS EXPLOITED! Critical damage surge!");
|
||||
|
|
@ -239,6 +335,18 @@ void CombatSystem::resolveShotImpact(Player& player) {
|
|||
addLog("Hit with [" + info.name + " " + tierData.tierBadge + "] for " + std::to_string(dealt) + " damage!");
|
||||
addFloatingText("-" + std::to_string(dealt), { 640, 240 }, info.color);
|
||||
|
||||
if (player.hasImplant(ImplantType::CATALYST_MATRIX)) {
|
||||
bool hasDebuff = false;
|
||||
for (const auto& st : m_enemy->getStatuses()) {
|
||||
if (st.effect == StatusEffect::BURNING || st.effect == StatusEffect::CORRODED) hasDebuff = true;
|
||||
}
|
||||
if (hasDebuff) {
|
||||
int catDmg = m_enemy->takeDamage(25, AmmoType::OVERHEAT);
|
||||
addLog("CATALYST MATRIX: Status detonated for +" + std::to_string(catDmg) + " bonus damage!");
|
||||
addFloatingText("+25 CATALYST", { 640, 160 }, CyberColors::NeonPink);
|
||||
}
|
||||
}
|
||||
|
||||
applyAmmoStatus(chamberAmmo, dealt, player);
|
||||
}
|
||||
|
||||
|
|
@ -291,10 +399,17 @@ void CombatSystem::executeEnemyTurn(Player& player) {
|
|||
m_shieldBoosted = false;
|
||||
}
|
||||
|
||||
int preShield = player.getShield();
|
||||
AmmoType elem = m_enemy->getAttackElement();
|
||||
int dealt = player.takeDamage(rawDmg, elem);
|
||||
SoundManager::get().playDamage();
|
||||
|
||||
if (preShield > 0 && player.getShield() == 0 && player.hasImplant(ImplantType::DEFLECTOR_FEEDBACK)) {
|
||||
int refDmg = m_enemy->takeDamage(20, AmmoType::EMP);
|
||||
addLog("DEFLECTOR FEEDBACK: Barrier shattered! Discharged 20 retaliatory shock damage!");
|
||||
addFloatingText("-20 SHOCK", { 640, 200 }, CyberColors::NeonCyan);
|
||||
}
|
||||
|
||||
addLog(m_enemy->getName() + " attacks with [" + getAmmoInfo(elem).shortName + "] for " + std::to_string(dealt) + " dmg!");
|
||||
addFloatingText("-" + std::to_string(dealt), { 300, 480 }, RED);
|
||||
|
||||
|
|
|
|||
|
|
@ -50,10 +50,15 @@ public:
|
|||
return m_phase == CombatPhase::VICTORY || m_phase == CombatPhase::DEFEAT;
|
||||
}
|
||||
bool isVictory() const { return m_phase == CombatPhase::VICTORY; }
|
||||
bool wasDefeatedEnemyElite() const { return m_defeatedEnemyWasElite; }
|
||||
bool wasElite() const { return m_defeatedEnemyWasElite; }
|
||||
|
||||
void addLog(const std::string& msg);
|
||||
void addFloatingText(const std::string& text, Vector2 pos, Color color);
|
||||
|
||||
AmmoType getLastFiredAmmo() const { return m_lastFiredAmmo; }
|
||||
float getPhaseTimer() const { return m_phaseTimer; }
|
||||
|
||||
private:
|
||||
void resolveShotImpact(Player& player);
|
||||
void resolveBurstShot(Player& player, int shotNum);
|
||||
|
|
@ -70,6 +75,10 @@ private:
|
|||
int m_burstShotsFired = 0;
|
||||
|
||||
bool m_shieldBoosted = false;
|
||||
bool m_ghostBonusDmg = false;
|
||||
bool m_defeatedEnemyWasElite = false;
|
||||
AmmoType m_lastFiredAmmo = AmmoType::EMPTY;
|
||||
|
||||
std::vector<std::string> m_combatLog;
|
||||
std::vector<FloatingText> m_floatingTexts;
|
||||
};
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@ Enemy::Enemy(EnemyType type, int level) : m_type(type), m_level(level) {
|
|||
m_baseDamage = 12 + level * 3;
|
||||
m_attackElement = AmmoType::OVERHEAT;
|
||||
m_weakness = AmmoType::EMP;
|
||||
m_color = CyberColors::NeonOrange;
|
||||
m_color = TacticalColors::PureWhite;
|
||||
break;
|
||||
|
||||
case EnemyType::ICE_SENTINEL:
|
||||
|
|
@ -22,7 +22,7 @@ Enemy::Enemy(EnemyType type, int level) : m_type(type), m_level(level) {
|
|||
m_baseDamage = 14 + level * 3;
|
||||
m_attackElement = AmmoType::EMP;
|
||||
m_weakness = AmmoType::CORROSIVE;
|
||||
m_color = CyberColors::NeonCyan;
|
||||
m_color = TacticalColors::Crimson;
|
||||
break;
|
||||
|
||||
case EnemyType::GLITCH_WRAITH:
|
||||
|
|
@ -32,7 +32,7 @@ Enemy::Enemy(EnemyType type, int level) : m_type(type), m_level(level) {
|
|||
m_baseDamage = 18 + level * 4;
|
||||
m_attackElement = AmmoType::GLITCH;
|
||||
m_weakness = AmmoType::KINETIC;
|
||||
m_color = CyberColors::NeonPurple;
|
||||
m_color = TacticalColors::BrightRed;
|
||||
break;
|
||||
|
||||
case EnemyType::VIRUS_CONSTRUCT:
|
||||
|
|
@ -42,7 +42,7 @@ Enemy::Enemy(EnemyType type, int level) : m_type(type), m_level(level) {
|
|||
m_baseDamage = 15 + level * 3;
|
||||
m_attackElement = AmmoType::CORROSIVE;
|
||||
m_weakness = AmmoType::OVERHEAT;
|
||||
m_color = CyberColors::NeonGreen;
|
||||
m_color = TacticalColors::DarkRed;
|
||||
break;
|
||||
}
|
||||
|
||||
|
|
@ -137,4 +137,17 @@ bool Enemy::isStunned() const {
|
|||
return false;
|
||||
}
|
||||
|
||||
void Enemy::setElite(bool elite) {
|
||||
if (m_isElite == elite) return;
|
||||
m_isElite = elite;
|
||||
if (m_isElite) {
|
||||
m_name = "[ELITE] " + m_name;
|
||||
m_maxHp = static_cast<int>(m_maxHp * 1.5f);
|
||||
m_hp = m_maxHp;
|
||||
m_maxShield = static_cast<int>(m_maxShield * 1.3f);
|
||||
m_shield = m_maxShield;
|
||||
m_baseDamage = static_cast<int>(m_baseDamage * 1.25f);
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace dnr
|
||||
|
|
|
|||
|
|
@ -29,6 +29,8 @@ public:
|
|||
Color getColor() const { return m_color; }
|
||||
|
||||
bool isAlive() const { return m_hp > 0; }
|
||||
bool isElite() const { return m_isElite; }
|
||||
void setElite(bool elite);
|
||||
|
||||
// Combat interactions
|
||||
int takeDamage(int amount, AmmoType element);
|
||||
|
|
@ -53,6 +55,7 @@ private:
|
|||
AmmoType m_attackElement = AmmoType::OVERHEAT;
|
||||
AmmoType m_weakness = AmmoType::EMP;
|
||||
Color m_color = RED;
|
||||
bool m_isElite = false;
|
||||
|
||||
std::vector<ActiveStatus> m_statuses;
|
||||
};
|
||||
|
|
|
|||
|
|
@ -44,13 +44,13 @@ struct AmmoInfo {
|
|||
};
|
||||
|
||||
inline const AmmoInfo& getAmmoInfo(AmmoType type) {
|
||||
static const AmmoInfo infoEmpty { AmmoType::EMPTY, "Empty Shell", "BLANK", "Empty chamber - no round.", DARKGRAY, 0, 0.0f };
|
||||
static const AmmoInfo infoKinetic { AmmoType::KINETIC, "Kinetic Pierce", "KINET", "Heavy kinetic sabot. High raw impact.", { 240, 240, 240, 255 }, 35, 0.25f };
|
||||
static const AmmoInfo infoEMP { AmmoType::EMP, "EMP Shock", "EMP", "Electro-magnetic pulse. Destroys shields, stuns machine.", { 0, 220, 255, 255 }, 20, 0.70f };
|
||||
static const AmmoInfo infoOverheat { AmmoType::OVERHEAT, "Overheat Core", "THERM", "Thermal incendiary round. Ignites target CPU for 3 turns.", { 255, 80, 20, 255 }, 18, 1.0f };
|
||||
static const AmmoInfo infoCorrosive { AmmoType::CORROSIVE, "Acid Corrosive", "ACID", "Sub-routine dissolver. Destroys armor, targets take +50% dmg.", { 120, 255, 0, 255 }, 15, 1.0f };
|
||||
static const AmmoInfo infoGlitch { AmmoType::GLITCH, "Glitch Cryo", "GLITCH", "Corrupting code payload. Drops enemy accuracy drastically.", { 220, 50, 255, 255 }, 22, 0.80f };
|
||||
static const AmmoInfo infoNanite { AmmoType::NANITE, "Nanite Siphon", "NANITE", "Vampiric micro-drones. Deals damage & restores Player HP/Shield.", { 0, 255, 180, 255 }, 16, 1.0f };
|
||||
static const AmmoInfo infoEmpty { AmmoType::EMPTY, "Empty Shell", "---", "Empty chamber - no round.", { 50, 60, 75, 255 }, 0, 0.0f };
|
||||
static const AmmoInfo infoKinetic { AmmoType::KINETIC, "Kinetic Pierce", "KIN", "Heavy kinetic sabot. High raw impact.", { 240, 245, 255, 255 }, 35, 0.25f };
|
||||
static const AmmoInfo infoEMP { AmmoType::EMP, "EMP Shock", "EMP", "Electro-magnetic pulse. Destroys shields, stuns machine.", { 70, 150, 255, 255 }, 20, 0.70f };
|
||||
static const AmmoInfo infoOverheat { AmmoType::OVERHEAT, "Overheat Core", "THM", "Thermal incendiary round. Ignites target CPU for 3 turns.", { 245, 130, 32, 255 }, 18, 1.0f };
|
||||
static const AmmoInfo infoCorrosive { AmmoType::CORROSIVE, "Acid Corrosive", "ACD", "Sub-routine dissolver. Destroys armor, targets take +50% dmg.", { 95, 185, 75, 255 }, 15, 1.0f };
|
||||
static const AmmoInfo infoGlitch { AmmoType::GLITCH, "Glitch Cryo", "GLT", "Corrupting code payload. Drops enemy accuracy drastically.", { 175, 95, 245, 255 }, 22, 0.80f };
|
||||
static const AmmoInfo infoNanite { AmmoType::NANITE, "Nanite Siphon", "NAN", "Vampiric micro-drones. Deals damage & restores Player HP/Shield.", { 225, 25, 55, 255 }, 16, 1.0f };
|
||||
|
||||
switch (type) {
|
||||
case AmmoType::KINETIC: return infoKinetic;
|
||||
|
|
@ -209,18 +209,159 @@ struct ActiveStatus {
|
|||
int power = 0;
|
||||
};
|
||||
|
||||
// Cyberpunk color palette
|
||||
namespace CyberColors {
|
||||
inline const Color NeonCyan = { 0, 240, 255, 255 };
|
||||
inline const Color NeonPink = { 255, 0, 128, 255 };
|
||||
inline const Color NeonGreen = { 57, 255, 20, 255 };
|
||||
inline const Color NeonYellow = { 255, 230, 0, 255 };
|
||||
inline const Color NeonOrange = { 255, 120, 0, 255 };
|
||||
inline const Color NeonPurple = { 180, 0, 255, 255 };
|
||||
inline const Color DarkGrid = { 10, 14, 26, 255 };
|
||||
inline const Color TerminalBg = { 6, 8, 15, 240 };
|
||||
inline const Color WallBorder = { 30, 70, 120, 255 };
|
||||
inline const Color WallGlow = { 0, 180, 255, 80 };
|
||||
// -------------------------------------------------------------
|
||||
// Neural Implants (Roguelike Buildcrafting Relics)
|
||||
// -------------------------------------------------------------
|
||||
enum class ImplantType {
|
||||
GHOST_CHAMBER = 0,
|
||||
ADRENALINE_DRUM,
|
||||
HEAVY_HAMMER,
|
||||
CATALYST_MATRIX,
|
||||
CONDUCTIVE_SALVO,
|
||||
CORROSIVE_LEECH,
|
||||
DEFLECTOR_FEEDBACK,
|
||||
AUTO_DEFIBRILLATOR,
|
||||
NANITE_WEAVER,
|
||||
LOGIC_OVERCLOCK,
|
||||
SALVAGE_PROTOCOL,
|
||||
PRECOGNITION_OPTICS,
|
||||
COUNT
|
||||
};
|
||||
|
||||
enum class ImplantArchetype {
|
||||
CYLINDER,
|
||||
ELEMENTAL,
|
||||
DEFENSE,
|
||||
HACKER
|
||||
};
|
||||
|
||||
struct ImplantData {
|
||||
ImplantType type = ImplantType::GHOST_CHAMBER;
|
||||
std::string name;
|
||||
std::string archetype; // "ROULETTE", "ELEMENTAL", "DEFENSE", "HACKER"
|
||||
ImplantArchetype archetypeEnum = ImplantArchetype::DEFENSE;
|
||||
std::string description;
|
||||
Color color = WHITE;
|
||||
std::string iconTag;
|
||||
std::string code;
|
||||
};
|
||||
|
||||
// Tactical Military / Arasaka Brutalism color palette (Monochrome + Crimson Red)
|
||||
namespace TacticalColors {
|
||||
// Backgrounds & Base Panels
|
||||
inline const Color PitchBlack = { 6, 8, 12, 255 }; // Deepest background
|
||||
inline const Color DarkPanel = { 12, 16, 22, 240 }; // Modal & panel backgrounds
|
||||
inline const Color SurfacePlate = { 20, 26, 36, 255 }; // Button & card backgrounds
|
||||
inline const Color SteelMuted = { 45, 55, 70, 255 }; // Dividers, disabled frames
|
||||
|
||||
// Monochrome Typography & Contours
|
||||
inline const Color PureWhite = { 255, 255, 255, 255 }; // High-contrast headers, active labels
|
||||
inline const Color SilverText = { 210, 218, 230, 255 }; // Normal body text
|
||||
inline const Color MutedText = { 120, 132, 148, 255 }; // Subtitles, inactive hints
|
||||
inline const Color BorderMuted = { 60, 72, 90, 255 }; // Unfocused border
|
||||
inline const Color BorderWhite = { 220, 230, 245, 255 }; // Focused / hover border
|
||||
|
||||
// Crimson & Tactical Red Accents
|
||||
inline const Color Crimson = { 230, 25, 45, 255 }; // Primary red accent, reticles, striker
|
||||
inline const Color BrightRed = { 255, 60, 75, 255 }; // Critical hit, warnings, alerts
|
||||
inline const Color DarkRed = { 70, 15, 22, 255 }; // Warning backing, alarm panels
|
||||
inline const Color CrimsonGlow = { 230, 25, 45, 90 }; // Low alpha red accent
|
||||
}
|
||||
|
||||
// Backward-compatibility alias namespace for gradual migration
|
||||
namespace CyberColors {
|
||||
inline const Color NeonCyan = TacticalColors::BorderWhite;
|
||||
inline const Color NeonPink = TacticalColors::Crimson;
|
||||
inline const Color NeonGreen = TacticalColors::PureWhite;
|
||||
inline const Color NeonYellow = TacticalColors::Crimson;
|
||||
inline const Color NeonOrange = TacticalColors::BrightRed;
|
||||
inline const Color NeonPurple = TacticalColors::SilverText;
|
||||
inline const Color DarkGrid = TacticalColors::PitchBlack;
|
||||
inline const Color TerminalBg = TacticalColors::DarkPanel;
|
||||
inline const Color WallBorder = TacticalColors::SteelMuted;
|
||||
inline const Color WallGlow = TacticalColors::CrimsonGlow;
|
||||
}
|
||||
|
||||
inline ImplantData getImplantData(ImplantType type) {
|
||||
switch (type) {
|
||||
case ImplantType::GHOST_CHAMBER:
|
||||
return { type, "GHOST CHAMBER", "ROULETTE", ImplantArchetype::CYLINDER,
|
||||
"Empty chamber gives +20 Shield and +50% damage on next shot.",
|
||||
TacticalColors::PureWhite, "[GHOST]", "GHOST" };
|
||||
case ImplantType::ADRENALINE_DRUM:
|
||||
return { type, "ADRENALINE DRUM", "ROULETTE", ImplantArchetype::CYLINDER,
|
||||
"Killing a drone instantly loads +1 random ammo & grants a free turn.",
|
||||
TacticalColors::Crimson, "[ADREN]", "ADREN" };
|
||||
case ImplantType::HEAVY_HAMMER:
|
||||
return { type, "HEAVY HAMMER", "ROULETTE", ImplantArchetype::CYLINDER,
|
||||
"Single shots deal +30% damage. Triple burst cooldown reduced to 1 turn.",
|
||||
TacticalColors::SilverText, "[HAMMR]", "HAMMR" };
|
||||
case ImplantType::CATALYST_MATRIX:
|
||||
return { type, "CATALYST MATRIX", "ELEMENTAL", ImplantArchetype::ELEMENTAL,
|
||||
"Attacking Burned or Corroded foes detonates for +25 bonus direct damage.",
|
||||
TacticalColors::Crimson, "[CATLY]", "CATLY" };
|
||||
case ImplantType::CONDUCTIVE_SALVO:
|
||||
return { type, "CONDUCTIVE SALVO", "ELEMENTAL", ImplantArchetype::ELEMENTAL,
|
||||
"EMP shots deal damage through enemy shields; Stun duration +1 turn.",
|
||||
TacticalColors::PureWhite, "[CONDC]", "CONDC" };
|
||||
case ImplantType::CORROSIVE_LEECH:
|
||||
return { type, "CORROSIVE LEECH", "ELEMENTAL", ImplantArchetype::ELEMENTAL,
|
||||
"Nanite hits on Corroded targets restore 100% more HP & Shield.",
|
||||
TacticalColors::BrightRed, "[LEECH]", "LEECH" };
|
||||
case ImplantType::DEFLECTOR_FEEDBACK:
|
||||
return { type, "DEFLECTOR FEEDBACK", "DEFENSE", ImplantArchetype::DEFENSE,
|
||||
"When Shield breaks, a shock pulse deals 20 retaliatory damage to drone.",
|
||||
TacticalColors::SilverText, "[DEFLC]", "DEFLC" };
|
||||
case ImplantType::AUTO_DEFIBRILLATOR:
|
||||
return { type, "AUTO-DEFIBRILLATOR", "DEFENSE", ImplantArchetype::DEFENSE,
|
||||
"Once per run, lethal damage is prevented: grants 1 HP, 50 Shield, and stuns.",
|
||||
TacticalColors::PureWhite, "[DEFIB]", "DEFIB" };
|
||||
case ImplantType::NANITE_WEAVER:
|
||||
return { type, "NANITE WEAVER", "DEFENSE", ImplantArchetype::DEFENSE,
|
||||
"Start of turn in combat: restore +5 HP and +8 Shield if enemy has debuffs.",
|
||||
TacticalColors::Crimson, "[WEAVR]", "WEAVR" };
|
||||
case ImplantType::LOGIC_OVERCLOCK:
|
||||
return { type, "LOGIC OVERCLOCK", "HACKER", ImplantArchetype::HACKER,
|
||||
"Skill check Great zone is 2x wider; Critical hacking awards 2x Core Chips.",
|
||||
TacticalColors::BrightRed, "[LOGIC]", "LOGIC" };
|
||||
case ImplantType::SALVAGE_PROTOCOL:
|
||||
return { type, "SALVAGE PROTOCOL", "HACKER", ImplantArchetype::HACKER,
|
||||
"+50% Core Chips dropped from all sources; 20% ammo chance on door bypass.",
|
||||
TacticalColors::PureWhite, "[SALVG]", "SALVG" };
|
||||
case ImplantType::PRECOGNITION_OPTICS:
|
||||
return { type, "PRECOGNITION OPTICS", "HACKER", ImplantArchetype::HACKER,
|
||||
"Scans 5 tiles ahead, revealing Elites, Mines, Lasers & Caches on minimap.",
|
||||
TacticalColors::SilverText, "[OPTIC]", "OPTIC" };
|
||||
default:
|
||||
return { type, "UNKNOWN IMPLANT", "GENERAL", ImplantArchetype::DEFENSE, "Undocumented firmware.", TacticalColors::PureWhite, "[CHIP]", "CHIP" };
|
||||
}
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------
|
||||
// Dungeon Interactive Hazards & Shrines
|
||||
// -------------------------------------------------------------
|
||||
struct LaserBarrier {
|
||||
int x = 0;
|
||||
int y = 0;
|
||||
bool vertical = false; // true = stretches along Z, false = stretches along X
|
||||
bool active = true;
|
||||
float timer = 0.0f;
|
||||
float period = 3.5f;
|
||||
bool disarmed = false;
|
||||
};
|
||||
|
||||
struct ProximityMine {
|
||||
int x = 0;
|
||||
int y = 0;
|
||||
bool defused = false;
|
||||
bool triggered = false;
|
||||
bool isArmed() const { return !defused && !triggered; }
|
||||
};
|
||||
|
||||
struct CyberShrine {
|
||||
int x = 0;
|
||||
int y = 0;
|
||||
bool used = false;
|
||||
};
|
||||
|
||||
} // namespace dnr
|
||||
|
|
|
|||
|
|
@ -23,6 +23,12 @@ void Game::init() {
|
|||
void Game::startNewRun() {
|
||||
m_dungeon.setFloor(1);
|
||||
m_player.reset(3, 4, Direction::NORTH);
|
||||
m_player.resetImplants();
|
||||
m_showImplantDraft = false;
|
||||
m_showShrineUI = false;
|
||||
m_activeShrine = nullptr;
|
||||
m_isDefusingMine = false;
|
||||
m_activeMine = nullptr;
|
||||
m_hubRoom.init(m_player);
|
||||
m_state = GameState::HUB;
|
||||
SoundManager::get().playTrack(MusicTrack::MENU_HUB);
|
||||
|
|
@ -86,6 +92,19 @@ bool Game::loadSavedProgress() {
|
|||
m_player.setAmmoTier(static_cast<AmmoType>(i), data.ammoTiers[i]);
|
||||
}
|
||||
|
||||
// Restore Neural Implants & Defibrillator state
|
||||
std::vector<ImplantType> imps;
|
||||
for (int val : data.implants) {
|
||||
imps.push_back(static_cast<ImplantType>(val));
|
||||
}
|
||||
m_player.setImplants(imps);
|
||||
m_player.setDefibrillatorUsed(data.defibrillatorUsed);
|
||||
m_showImplantDraft = false;
|
||||
m_showShrineUI = false;
|
||||
m_activeShrine = nullptr;
|
||||
m_isDefusingMine = false;
|
||||
m_activeMine = nullptr;
|
||||
|
||||
if (data.inHub) {
|
||||
m_hubRoom.init(m_player);
|
||||
m_state = GameState::HUB;
|
||||
|
|
@ -108,6 +127,12 @@ void Game::update(float dt) {
|
|||
m_saveFeedbackTimer -= dt;
|
||||
}
|
||||
|
||||
// Update screen transition wave
|
||||
m_transition.update(dt);
|
||||
if (m_transition.isBusy()) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Update dynamic music streaming engine every frame
|
||||
SoundManager::get().updateMusic(dt);
|
||||
|
||||
|
|
@ -141,12 +166,12 @@ void Game::update(float dt) {
|
|||
break;
|
||||
case GameState::VICTORY_LEVEL:
|
||||
if (IsKeyPressed(KEY_SPACE) || IsKeyPressed(KEY_ENTER)) {
|
||||
enterRestRoom();
|
||||
m_transition.start([this]() { enterRestRoom(); });
|
||||
}
|
||||
break;
|
||||
case GameState::GAME_OVER:
|
||||
if (IsKeyPressed(KEY_SPACE) || IsKeyPressed(KEY_ENTER)) {
|
||||
startNewRun();
|
||||
m_transition.start([this]() { startNewRun(); });
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
|
@ -166,9 +191,9 @@ void Game::updateMainMenu(float dt) {
|
|||
bool hasSave = SaveSystem::hasSaveFile();
|
||||
|
||||
if ((IsKeyPressed(KEY_ONE) || (mouseClicked && CheckCollisionPointRec(mouse, btnContinue))) && hasSave) {
|
||||
loadSavedProgress();
|
||||
m_transition.start([this]() { loadSavedProgress(); });
|
||||
} else if (IsKeyPressed(KEY_TWO) || (mouseClicked && CheckCollisionPointRec(mouse, btnNewRun))) {
|
||||
startNewRun();
|
||||
m_transition.start([this]() { startNewRun(); });
|
||||
} else if (IsKeyPressed(KEY_THREE) || (mouseClicked && CheckCollisionPointRec(mouse, btnSettings))) {
|
||||
m_previousState = GameState::MAIN_MENU;
|
||||
m_state = GameState::SETTINGS;
|
||||
|
|
@ -238,7 +263,7 @@ void Game::tryMoveHub(int dx, int dy) {
|
|||
|
||||
void Game::tryInteractHub() {
|
||||
if (m_hubRoom.isFacingPortal(m_player)) {
|
||||
enterCyberspace();
|
||||
m_transition.start([this]() { enterCyberspace(); });
|
||||
} else if (m_hubRoom.isFacingTerminal(m_player)) {
|
||||
saveCurrentProgress();
|
||||
}
|
||||
|
|
@ -367,7 +392,7 @@ void Game::tryMoveRestRoom(int dx, int dy) {
|
|||
|
||||
void Game::tryInteractRestRoom() {
|
||||
if (m_restRoom.isFacingGateway(m_player)) {
|
||||
nextFloor();
|
||||
m_transition.start([this]() { nextFloor(); });
|
||||
} else if (m_restRoom.isFacingRechargeMatrix(m_player)) {
|
||||
if (!m_restRoom.isRecharged()) {
|
||||
m_restRoom.triggerRecharge(m_player);
|
||||
|
|
@ -381,10 +406,183 @@ void Game::tryInteractRestRoom() {
|
|||
}
|
||||
}
|
||||
|
||||
void Game::triggerImplantDraft() {
|
||||
std::vector<ImplantType> available;
|
||||
for (int i = 0; i < 12; ++i) {
|
||||
ImplantType t = static_cast<ImplantType>(i);
|
||||
if (!m_player.hasImplant(t)) {
|
||||
available.push_back(t);
|
||||
}
|
||||
}
|
||||
if (available.empty()) {
|
||||
m_player.addTechScraps(50);
|
||||
return;
|
||||
}
|
||||
for (size_t i = available.size() - 1; i > 0; --i) {
|
||||
size_t j = std::rand() % (i + 1);
|
||||
std::swap(available[i], available[j]);
|
||||
}
|
||||
m_draftChoices.clear();
|
||||
for (size_t i = 0; i < available.size() && i < 3; ++i) {
|
||||
m_draftChoices.push_back(available[i]);
|
||||
}
|
||||
m_showImplantDraft = true;
|
||||
m_draftReplaceSlot = -1;
|
||||
SoundManager::get().playSkillGreat();
|
||||
}
|
||||
|
||||
void Game::handleImplantDraftInput() {
|
||||
Vector2 mouse = GetMousePosition();
|
||||
bool mouseClicked = IsMouseButtonPressed(MOUSE_BUTTON_LEFT);
|
||||
int screenW = GetScreenWidth();
|
||||
int screenH = GetScreenHeight();
|
||||
|
||||
if (m_player.getImplants().size() >= 4) {
|
||||
int modalW = 960;
|
||||
int modalH = 550;
|
||||
int modalX = (screenW - modalW) / 2;
|
||||
int modalY = (screenH - modalH) / 2;
|
||||
int mountY = modalY + 332;
|
||||
int slotW = 212;
|
||||
int slotH = 68;
|
||||
int slotGap = 16;
|
||||
int startSlotX = modalX + 30;
|
||||
|
||||
for (int s = 0; s < 4; ++s) {
|
||||
int slX = startSlotX + s * (slotW + slotGap);
|
||||
int slY = mountY + 20;
|
||||
Rectangle slotRect = { static_cast<float>(slX), static_cast<float>(slY), static_cast<float>(slotW), static_cast<float>(slotH) };
|
||||
if (mouseClicked && CheckCollisionPointRec(mouse, slotRect)) {
|
||||
m_draftReplaceSlot = s;
|
||||
SoundManager::get().playTumblerClick();
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (size_t i = 0; i < m_draftChoices.size() && i < 3; ++i) {
|
||||
Rectangle btn = UIRenderer::getDraftCardButtonRect(static_cast<int>(i), screenW, screenH);
|
||||
bool keyPressed = (i == 0 && IsKeyPressed(KEY_ONE)) ||
|
||||
(i == 1 && IsKeyPressed(KEY_TWO)) ||
|
||||
(i == 2 && IsKeyPressed(KEY_THREE));
|
||||
|
||||
if (keyPressed || (mouseClicked && CheckCollisionPointRec(mouse, btn))) {
|
||||
ImplantType chosen = m_draftChoices[i];
|
||||
if (m_player.getImplants().size() < 4) {
|
||||
m_player.addImplant(chosen);
|
||||
m_showImplantDraft = false;
|
||||
SoundManager::get().playPickup();
|
||||
return;
|
||||
} else {
|
||||
if (m_draftReplaceSlot >= 0 && m_draftReplaceSlot < 4) {
|
||||
m_player.replaceImplant(m_draftReplaceSlot, chosen);
|
||||
m_showImplantDraft = false;
|
||||
m_draftReplaceSlot = -1;
|
||||
SoundManager::get().playPickup();
|
||||
return;
|
||||
} else {
|
||||
SoundManager::get().playDamage();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle scrapBtn = UIRenderer::getDraftScrapButtonRect(screenW, screenH);
|
||||
if (IsKeyPressed(KEY_FOUR) || IsKeyPressed(KEY_ESCAPE) || (mouseClicked && CheckCollisionPointRec(mouse, scrapBtn))) {
|
||||
m_player.addTechScraps(35);
|
||||
m_showImplantDraft = false;
|
||||
m_draftReplaceSlot = -1;
|
||||
SoundManager::get().playTumblerClick();
|
||||
}
|
||||
}
|
||||
|
||||
void Game::handleShrineInput() {
|
||||
Vector2 mouse = GetMousePosition();
|
||||
bool mouseClicked = IsMouseButtonPressed(MOUSE_BUTTON_LEFT);
|
||||
int screenW = GetScreenWidth();
|
||||
int screenH = GetScreenHeight();
|
||||
|
||||
Rectangle btn0 = UIRenderer::getShrineOptionButtonRect(0, screenW, screenH);
|
||||
if (IsKeyPressed(KEY_ONE) || (mouseClicked && CheckCollisionPointRec(mouse, btn0))) {
|
||||
if (m_player.getHp() > 25) {
|
||||
m_player.takeDamage(25);
|
||||
m_player.addTechScraps(60);
|
||||
m_player.addAmmo(AmmoType::KINETIC, 1);
|
||||
m_player.addAmmo(AmmoType::GLITCH, 1);
|
||||
if (m_activeShrine) m_activeShrine->used = true;
|
||||
m_showShrineUI = false;
|
||||
m_activeShrine = nullptr;
|
||||
SoundManager::get().playSkillGreat();
|
||||
return;
|
||||
} else {
|
||||
SoundManager::get().playDamage();
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle btn1 = UIRenderer::getShrineOptionButtonRect(1, screenW, screenH);
|
||||
if (IsKeyPressed(KEY_TWO) || (mouseClicked && CheckCollisionPointRec(mouse, btn1))) {
|
||||
if (m_player.getCyberPicks() >= 1) {
|
||||
m_player.usePick();
|
||||
m_player.increaseMaxShield(15);
|
||||
m_player.rechargeShield(m_player.getMaxShield());
|
||||
if (m_activeShrine) m_activeShrine->used = true;
|
||||
m_showShrineUI = false;
|
||||
m_activeShrine = nullptr;
|
||||
SoundManager::get().playSkillGreat();
|
||||
return;
|
||||
} else {
|
||||
SoundManager::get().playDamage();
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle btn2 = UIRenderer::getShrineOptionButtonRect(2, screenW, screenH);
|
||||
if (IsKeyPressed(KEY_THREE) || (mouseClicked && CheckCollisionPointRec(mouse, btn2))) {
|
||||
if (m_player.getAmmoCount(AmmoType::KINETIC) >= 2) {
|
||||
m_player.consumeAmmo(AmmoType::KINETIC);
|
||||
m_player.consumeAmmo(AmmoType::KINETIC);
|
||||
m_player.addAmmo(AmmoType::NANITE, 1);
|
||||
m_player.addAmmo(AmmoType::GLITCH, 1);
|
||||
if (m_activeShrine) m_activeShrine->used = true;
|
||||
m_showShrineUI = false;
|
||||
m_activeShrine = nullptr;
|
||||
SoundManager::get().playSkillGreat();
|
||||
return;
|
||||
} else {
|
||||
SoundManager::get().playDamage();
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle btnClose = UIRenderer::getShrineCloseButtonRect(screenW, screenH);
|
||||
if (IsKeyPressed(KEY_FOUR) || IsKeyPressed(KEY_ESCAPE) || (mouseClicked && CheckCollisionPointRec(mouse, btnClose))) {
|
||||
m_showShrineUI = false;
|
||||
m_activeShrine = nullptr;
|
||||
SoundManager::get().playTumblerClick();
|
||||
}
|
||||
}
|
||||
|
||||
void Game::updateExploration(float dt) {
|
||||
if (m_showImplantDraft) {
|
||||
handleImplantDraftInput();
|
||||
return;
|
||||
}
|
||||
if (m_showShrineUI) {
|
||||
handleShrineInput();
|
||||
return;
|
||||
}
|
||||
|
||||
m_player.update(dt);
|
||||
m_dungeon.reveal(m_player.getGridX(), m_player.getGridY(), 3);
|
||||
|
||||
// Update laser barrier timers
|
||||
for (auto& laser : m_dungeon.getLasers()) {
|
||||
if (laser.disarmed) continue;
|
||||
laser.timer += dt;
|
||||
if (laser.timer >= laser.period) {
|
||||
laser.timer -= laser.period;
|
||||
}
|
||||
laser.active = (laser.timer < 2.0f);
|
||||
}
|
||||
|
||||
Vector2 dirVec = getDirectionOffset(m_player.getFacing());
|
||||
int frontX = m_player.getGridX() + static_cast<int>(dirVec.x);
|
||||
int frontY = m_player.getGridY() + static_cast<int>(dirVec.y);
|
||||
|
|
@ -392,12 +590,25 @@ void Game::updateExploration(float dt) {
|
|||
m_contextPrompt = "";
|
||||
if (m_dungeon.isDoorLocked(frontX, frontY)) {
|
||||
m_contextPrompt = "[SPACE / E] Bypass Door ICE [Picks: " + std::to_string(m_player.getCyberPicks()) + "]";
|
||||
} else if (m_dungeon.getTile(frontX, frontY) == TileType::LOOT_CACHE ||
|
||||
m_dungeon.getTile(m_player.getGridX(), m_player.getGridY()) == TileType::LOOT_CACHE) {
|
||||
m_contextPrompt = "[SPACE / E] Extract Data Cache";
|
||||
} else if (m_dungeon.getTile(m_player.getGridX(), m_player.getGridY()) == TileType::EXIT_PORTAL ||
|
||||
m_dungeon.getTile(frontX, frontY) == TileType::EXIT_PORTAL) {
|
||||
m_contextPrompt = "[SPACE / E] Jack Out To Safe Haven Rest Node";
|
||||
} else {
|
||||
LaserBarrier* frontLaser = m_dungeon.getLaserAt(frontX, frontY);
|
||||
ProximityMine* frontMine = m_dungeon.getMineAt(frontX, frontY);
|
||||
CyberShrine* frontShrine = m_dungeon.getShrineAt(frontX, frontY);
|
||||
if (!frontShrine) frontShrine = m_dungeon.getShrineAt(m_player.getGridX(), m_player.getGridY());
|
||||
|
||||
if (frontLaser && !frontLaser->disarmed) {
|
||||
m_contextPrompt = "[SPACE / E] Disarm Laser Barrier (Cost: 1 ICE Pick)";
|
||||
} else if (frontMine && frontMine->isArmed()) {
|
||||
m_contextPrompt = "[SPACE / E] Defuse Proximity Mine (Quick DBD Timing Check)";
|
||||
} else if (frontShrine && !frontShrine->used) {
|
||||
m_contextPrompt = "[SPACE / E] Access Darknet Cyber Shrine";
|
||||
} else if (m_dungeon.getTile(frontX, frontY) == TileType::LOOT_CACHE ||
|
||||
m_dungeon.getTile(m_player.getGridX(), m_player.getGridY()) == TileType::LOOT_CACHE) {
|
||||
m_contextPrompt = "[SPACE / E] Extract Data Cache";
|
||||
} else if (m_dungeon.getTile(m_player.getGridX(), m_player.getGridY()) == TileType::EXIT_PORTAL ||
|
||||
m_dungeon.getTile(frontX, frontY) == TileType::EXIT_PORTAL) {
|
||||
m_contextPrompt = "[SPACE / E] Jack Out To Safe Haven Rest Node";
|
||||
}
|
||||
}
|
||||
|
||||
if (IsKeyPressed(KEY_W) || IsKeyPressed(KEY_UP)) {
|
||||
|
|
@ -448,7 +659,9 @@ void Game::updateExploration(float dt) {
|
|||
m_showCombatCodex = false;
|
||||
m_lastPlayerHp = m_player.getHp();
|
||||
SoundManager::get().playTrack(MusicTrack::COMBAT);
|
||||
m_combat.startCombat(m_player, std::make_unique<Enemy>(me->enemy.getType(), m_dungeon.getFloor()));
|
||||
auto enemyPtr = std::make_unique<Enemy>(me->enemy.getType(), m_dungeon.getFloor());
|
||||
if (me->enemy.isElite()) enemyPtr->setElite(true);
|
||||
m_combat.startCombat(m_player, std::move(enemyPtr));
|
||||
m_dungeon.removeEnemyAt(me->x, me->y);
|
||||
}
|
||||
}
|
||||
|
|
@ -466,8 +679,36 @@ void Game::tryMoveDungeon(int dx, int dy) {
|
|||
m_showCombatCodex = false;
|
||||
m_lastPlayerHp = m_player.getHp();
|
||||
SoundManager::get().playTrack(MusicTrack::COMBAT);
|
||||
m_combat.startCombat(m_player, std::make_unique<Enemy>(me->enemy.getType(), m_dungeon.getFloor()));
|
||||
auto enemyPtr = std::make_unique<Enemy>(me->enemy.getType(), m_dungeon.getFloor());
|
||||
if (me->enemy.isElite()) enemyPtr->setElite(true);
|
||||
m_combat.startCombat(m_player, std::move(enemyPtr));
|
||||
m_dungeon.removeEnemyAt(nx, ny);
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if stepped on laser barrier
|
||||
LaserBarrier* lb = m_dungeon.getLaserAt(nx, ny);
|
||||
if (lb && !lb->disarmed && lb->active) {
|
||||
m_player.takeDamage(15, AmmoType::EMPTY);
|
||||
m_player.applyStatus(StatusEffect::GLITCHED, 2, 1);
|
||||
SoundManager::get().playDamage();
|
||||
if (m_player.isDead()) {
|
||||
m_state = GameState::GAME_OVER;
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Check if stepped on proximity mine
|
||||
ProximityMine* pm = m_dungeon.getMineAt(nx, ny);
|
||||
if (pm && pm->isArmed()) {
|
||||
pm->triggered = true;
|
||||
m_player.takeDamage(25, AmmoType::EMPTY);
|
||||
m_player.applyStatus(StatusEffect::GLITCHED, 2, 1);
|
||||
SoundManager::get().playDamage();
|
||||
if (m_player.isDead()) {
|
||||
m_state = GameState::GAME_OVER;
|
||||
return;
|
||||
}
|
||||
}
|
||||
} else if (m_dungeon.isDoorLocked(nx, ny)) {
|
||||
m_targetDoorX = nx;
|
||||
|
|
@ -489,7 +730,8 @@ void Game::tryInteractDungeon() {
|
|||
m_targetDoorY = fy;
|
||||
DoorData* dd = m_dungeon.getDoorAt(fx, fy);
|
||||
int sec = dd ? dd->securityLevel : 1;
|
||||
m_hacking.start(sec);
|
||||
bool expanded = m_player.hasImplant(ImplantType::LOGIC_OVERCLOCK);
|
||||
m_hacking.start(sec, expanded);
|
||||
m_state = GameState::HACKING;
|
||||
SoundManager::get().playTumblerClick();
|
||||
} else {
|
||||
|
|
@ -498,6 +740,38 @@ void Game::tryInteractDungeon() {
|
|||
return;
|
||||
}
|
||||
|
||||
LaserBarrier* lb = m_dungeon.getLaserAt(fx, fy);
|
||||
if (lb && !lb->disarmed) {
|
||||
if (m_player.getCyberPicks() > 0) {
|
||||
m_player.usePick();
|
||||
lb->disarmed = true;
|
||||
SoundManager::get().playDoorOpen();
|
||||
} else {
|
||||
SoundManager::get().playDamage();
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
ProximityMine* pm = m_dungeon.getMineAt(fx, fy);
|
||||
if (pm && pm->isArmed()) {
|
||||
m_activeMine = pm;
|
||||
m_isDefusingMine = true;
|
||||
bool expanded = m_player.hasImplant(ImplantType::LOGIC_OVERCLOCK);
|
||||
m_hacking.startSkillCheckOnly(1, expanded);
|
||||
m_state = GameState::HACKING;
|
||||
SoundManager::get().playTumblerClick();
|
||||
return;
|
||||
}
|
||||
|
||||
CyberShrine* cs = m_dungeon.getShrineAt(fx, fy);
|
||||
if (!cs) cs = m_dungeon.getShrineAt(px, py);
|
||||
if (cs && !cs->used) {
|
||||
m_activeShrine = cs;
|
||||
m_showShrineUI = true;
|
||||
SoundManager::get().playSkillGreat();
|
||||
return;
|
||||
}
|
||||
|
||||
LootData* ld = m_dungeon.getLootAt(fx, fy);
|
||||
if (!ld) ld = m_dungeon.getLootAt(px, py);
|
||||
|
||||
|
|
@ -515,7 +789,7 @@ void Game::tryInteractDungeon() {
|
|||
|
||||
if (m_dungeon.getTile(px, py) == TileType::EXIT_PORTAL ||
|
||||
m_dungeon.getTile(fx, fy) == TileType::EXIT_PORTAL) {
|
||||
enterRestRoom();
|
||||
m_transition.start([this]() { enterRestRoom(); });
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
|
@ -532,6 +806,37 @@ void Game::updateHacking(float dt) {
|
|||
m_hacking.handleInput();
|
||||
|
||||
if (m_hacking.isFinished() && m_hacking.getResultTimer() > 1.2f) {
|
||||
if (m_isDefusingMine) {
|
||||
m_isDefusingMine = false;
|
||||
if (m_hacking.isSuccess()) {
|
||||
if (m_activeMine) {
|
||||
m_activeMine->defused = true;
|
||||
}
|
||||
m_player.addTechScraps(20);
|
||||
m_player.addAmmo(AmmoType::KINETIC, 1);
|
||||
if (m_hacking.isCritical()) {
|
||||
m_player.addTechScraps(15);
|
||||
m_player.addAmmo(AmmoType::EMP, 1);
|
||||
}
|
||||
SoundManager::get().playPickup();
|
||||
} else {
|
||||
if (m_activeMine) {
|
||||
m_activeMine->triggered = true;
|
||||
}
|
||||
m_player.takeDamage(25, AmmoType::EMPTY);
|
||||
m_player.applyStatus(StatusEffect::GLITCHED, 2, 1);
|
||||
SoundManager::get().playDamage();
|
||||
if (m_player.isDead()) {
|
||||
m_activeMine = nullptr;
|
||||
m_state = GameState::GAME_OVER;
|
||||
return;
|
||||
}
|
||||
}
|
||||
m_activeMine = nullptr;
|
||||
m_state = GameState::EXPLORATION;
|
||||
return;
|
||||
}
|
||||
|
||||
if (m_hacking.isSuccess()) {
|
||||
m_dungeon.unlockDoorAt(m_targetDoorX, m_targetDoorY);
|
||||
SoundManager::get().playDoorOpen();
|
||||
|
|
@ -540,6 +845,9 @@ void Game::updateHacking(float dt) {
|
|||
m_player.addAmmo(AmmoType::NANITE, 1);
|
||||
m_player.addPicks(1);
|
||||
m_player.addTechScraps(40);
|
||||
if (m_hacking.getSecurityLevel() >= 3) {
|
||||
triggerImplantDraft();
|
||||
}
|
||||
} else {
|
||||
m_player.addTechScraps(15);
|
||||
}
|
||||
|
|
@ -628,11 +936,15 @@ void Game::updateCombat(float dt) {
|
|||
}
|
||||
} else if (m_combat.getPhase() == CombatPhase::VICTORY) {
|
||||
if (IsKeyPressed(KEY_SPACE) || IsKeyPressed(KEY_ENTER) || mouseClicked) {
|
||||
bool wasElite = m_combat.wasElite();
|
||||
m_player.heal(25);
|
||||
m_player.addAmmo(AmmoType::KINETIC, 1);
|
||||
m_player.addAmmo(AmmoType::EMP, 1);
|
||||
m_state = GameState::EXPLORATION;
|
||||
SoundManager::get().playTrack(MusicTrack::EXPLORATION, true);
|
||||
if (wasElite) {
|
||||
triggerImplantDraft();
|
||||
}
|
||||
}
|
||||
} else if (m_combat.getPhase() == CombatPhase::DEFEAT) {
|
||||
if (IsKeyPressed(KEY_SPACE) || IsKeyPressed(KEY_ENTER) || mouseClicked) {
|
||||
|
|
@ -795,6 +1107,11 @@ void Game::draw() {
|
|||
case GameState::EXPLORATION:
|
||||
m_renderer.render3D(m_dungeon, m_player, m_animTime);
|
||||
m_uiRenderer.drawExplorationHUD(m_dungeon, m_player, m_contextPrompt);
|
||||
if (m_showImplantDraft) {
|
||||
m_uiRenderer.drawImplantDraftModal(m_draftChoices, m_player, m_draftReplaceSlot);
|
||||
} else if (m_showShrineUI) {
|
||||
m_uiRenderer.drawShrineModal(m_player);
|
||||
}
|
||||
m_uiRenderer.drawMatrixOverlay(m_animTime);
|
||||
break;
|
||||
|
||||
|
|
@ -863,6 +1180,9 @@ void Game::draw() {
|
|||
break;
|
||||
}
|
||||
|
||||
// Render screen transition wave on top of all 2D/3D elements
|
||||
m_transition.draw();
|
||||
|
||||
EndDrawing();
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@
|
|||
#include "inventory/InventoryUI.hpp"
|
||||
#include "render/CyberRenderer.hpp"
|
||||
#include "render/UIRenderer.hpp"
|
||||
#include "render/ScreenTransition.hpp"
|
||||
#include "hub/HubRoom.hpp"
|
||||
#include "hub/RestRoom.hpp"
|
||||
#include "core/SaveSystem.hpp"
|
||||
|
|
@ -64,6 +65,7 @@ private:
|
|||
|
||||
CyberRenderer m_renderer;
|
||||
UIRenderer m_uiRenderer;
|
||||
ScreenTransition m_transition;
|
||||
|
||||
float m_animTime = 0.0f;
|
||||
float m_saveFeedbackTimer = 0.0f;
|
||||
|
|
@ -77,9 +79,23 @@ private:
|
|||
|
||||
bool m_showCombatCodex = false;
|
||||
bool m_showWorkbenchUI = false;
|
||||
int m_lastPlayerHp = 100;
|
||||
void triggerImplantDraft();
|
||||
void handleImplantDraftInput();
|
||||
void handleShrineInput();
|
||||
|
||||
int m_lastPlayerHp = 100;
|
||||
bool m_shouldQuit = false;
|
||||
|
||||
// Roguelike Implants, Shrines & Hazard states
|
||||
bool m_showImplantDraft = false;
|
||||
std::vector<ImplantType> m_draftChoices;
|
||||
int m_draftReplaceSlot = -1;
|
||||
|
||||
bool m_showShrineUI = false;
|
||||
CyberShrine* m_activeShrine = nullptr;
|
||||
|
||||
bool m_isDefusingMine = false;
|
||||
ProximityMine* m_activeMine = nullptr;
|
||||
};
|
||||
|
||||
} // namespace dnr
|
||||
|
|
|
|||
|
|
@ -38,6 +38,14 @@ bool SaveSystem::saveGame(const std::string& path, int floor, bool inHub, const
|
|||
}
|
||||
out << "\n";
|
||||
|
||||
out << "implants " << player.getImplants().size();
|
||||
for (auto imp : player.getImplants()) {
|
||||
out << " " << static_cast<int>(imp);
|
||||
}
|
||||
out << "\n";
|
||||
|
||||
out << "defibUsed " << (player.isDefibrillatorUsed() ? 1 : 0) << "\n";
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
|
|
@ -77,6 +85,19 @@ bool SaveSystem::loadGame(const std::string& path, SaveData& outData) {
|
|||
for (size_t i = 0; i < outData.ammoTiers.size(); ++i) {
|
||||
in >> outData.ammoTiers[i];
|
||||
}
|
||||
} else if (key == "implants") {
|
||||
int count = 0;
|
||||
in >> count;
|
||||
outData.implants.clear();
|
||||
for (int i = 0; i < count; ++i) {
|
||||
int impVal = 0;
|
||||
in >> impVal;
|
||||
outData.implants.push_back(impVal);
|
||||
}
|
||||
} else if (key == "defibUsed") {
|
||||
int du = 0;
|
||||
in >> du;
|
||||
outData.defibrillatorUsed = (du != 0);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -20,6 +20,8 @@ struct SaveData {
|
|||
std::array<int, 6> chambers{};
|
||||
std::array<int, 7> ammoCounts{};
|
||||
std::array<int, 7> ammoTiers{ 1, 1, 1, 1, 1, 1, 1 };
|
||||
std::vector<int> implants{};
|
||||
bool defibrillatorUsed = false;
|
||||
};
|
||||
|
||||
class SaveSystem {
|
||||
|
|
|
|||
|
|
@ -13,6 +13,9 @@ void Dungeon::clear() {
|
|||
m_doors.clear();
|
||||
m_loots.clear();
|
||||
m_enemies.clear();
|
||||
m_lasers.clear();
|
||||
m_mines.clear();
|
||||
m_shrines.clear();
|
||||
}
|
||||
|
||||
void Dungeon::resize(int width, int height) {
|
||||
|
|
@ -77,8 +80,59 @@ void Dungeon::addLoot(int x, int y, const std::vector<AmmoType>& ammo, int picks
|
|||
setTile(x, y, TileType::LOOT_CACHE);
|
||||
}
|
||||
|
||||
void Dungeon::addEnemy(int x, int y, EnemyType type, int level) {
|
||||
m_enemies.push_back({ x, y, Enemy(type, level), true });
|
||||
void Dungeon::addEnemy(int x, int y, EnemyType type, int level, bool elite) {
|
||||
Enemy en(type, level);
|
||||
if (elite) en.setElite(true);
|
||||
m_enemies.push_back({ x, y, en, true });
|
||||
}
|
||||
|
||||
void Dungeon::addLaser(int x, int y, bool vertical) {
|
||||
LaserBarrier lb;
|
||||
lb.x = x;
|
||||
lb.y = y;
|
||||
lb.vertical = vertical;
|
||||
lb.active = true;
|
||||
lb.timer = 0.0f;
|
||||
lb.disarmed = false;
|
||||
m_lasers.push_back(lb);
|
||||
}
|
||||
|
||||
void Dungeon::addMine(int x, int y) {
|
||||
ProximityMine pm;
|
||||
pm.x = x;
|
||||
pm.y = y;
|
||||
pm.defused = false;
|
||||
pm.triggered = false;
|
||||
m_mines.push_back(pm);
|
||||
}
|
||||
|
||||
void Dungeon::addShrine(int x, int y) {
|
||||
CyberShrine cs;
|
||||
cs.x = x;
|
||||
cs.y = y;
|
||||
cs.used = false;
|
||||
m_shrines.push_back(cs);
|
||||
}
|
||||
|
||||
LaserBarrier* Dungeon::getLaserAt(int x, int y) {
|
||||
for (auto& l : m_lasers) {
|
||||
if (l.x == x && l.y == y && !l.disarmed) return &l;
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
ProximityMine* Dungeon::getMineAt(int x, int y) {
|
||||
for (auto& m : m_mines) {
|
||||
if (m.x == x && m.y == y && !m.defused && !m.triggered) return &m;
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
CyberShrine* Dungeon::getShrineAt(int x, int y) {
|
||||
for (auto& s : m_shrines) {
|
||||
if (s.x == x && s.y == y) return &s;
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
void Dungeon::removeEnemyAt(int x, int y) {
|
||||
|
|
|
|||
|
|
@ -54,7 +54,15 @@ public:
|
|||
|
||||
void addDoor(int x, int y, int secLevel);
|
||||
void addLoot(int x, int y, const std::vector<AmmoType>& ammo, int picks);
|
||||
void addEnemy(int x, int y, EnemyType type, int level);
|
||||
void addEnemy(int x, int y, EnemyType type, int level, bool elite = false);
|
||||
|
||||
void addLaser(int x, int y, bool vertical);
|
||||
void addMine(int x, int y);
|
||||
void addShrine(int x, int y);
|
||||
|
||||
LaserBarrier* getLaserAt(int x, int y);
|
||||
ProximityMine* getMineAt(int x, int y);
|
||||
CyberShrine* getShrineAt(int x, int y);
|
||||
|
||||
void removeEnemyAt(int x, int y);
|
||||
void unlockDoorAt(int x, int y);
|
||||
|
|
@ -65,6 +73,13 @@ public:
|
|||
const std::vector<DoorData>& getDoors() const { return m_doors; }
|
||||
const std::vector<LootData>& getLoots() const { return m_loots; }
|
||||
std::vector<MapEnemy>& getEnemies() { return m_enemies; }
|
||||
const std::vector<MapEnemy>& getEnemies() const { return m_enemies; }
|
||||
std::vector<LaserBarrier>& getLasers() { return m_lasers; }
|
||||
const std::vector<LaserBarrier>& getLasers() const { return m_lasers; }
|
||||
std::vector<ProximityMine>& getMines() { return m_mines; }
|
||||
const std::vector<ProximityMine>& getMines() const { return m_mines; }
|
||||
std::vector<CyberShrine>& getShrines() { return m_shrines; }
|
||||
const std::vector<CyberShrine>& getShrines() const { return m_shrines; }
|
||||
|
||||
private:
|
||||
int m_width;
|
||||
|
|
@ -76,6 +91,9 @@ private:
|
|||
std::vector<DoorData> m_doors;
|
||||
std::vector<LootData> m_loots;
|
||||
std::vector<MapEnemy> m_enemies;
|
||||
std::vector<LaserBarrier> m_lasers;
|
||||
std::vector<ProximityMine> m_mines;
|
||||
std::vector<CyberShrine> m_shrines;
|
||||
};
|
||||
|
||||
} // namespace dnr
|
||||
|
|
|
|||
|
|
@ -224,12 +224,79 @@ DungeonGenResult DungeonGen::generate(Dungeon& dungeon, int floor) {
|
|||
dungeon.addLoot(lx, ly, rewards, picks);
|
||||
}
|
||||
|
||||
// Place enemy
|
||||
// Place enemy (at least 1-2 become ELITE)
|
||||
EnemyType eType = static_cast<EnemyType>(std::rand() % 4);
|
||||
int ex = cx;
|
||||
int ey = cy;
|
||||
if (dungeon.getTile(ex, ey) == TileType::FLOOR && (ex != res.exitX || ey != res.exitY) && (ex != res.startX || ey != res.startY)) {
|
||||
dungeon.addEnemy(ex, ey, eType, floor);
|
||||
bool isElite = (i == rooms.size() - 1) || (floor >= 2 && i == 2);
|
||||
dungeon.addEnemy(ex, ey, eType, floor, isElite);
|
||||
}
|
||||
}
|
||||
|
||||
// Place 1 Cyber Shrine in a room distinct from start and exit
|
||||
if (rooms.size() >= 3) {
|
||||
size_t shrineRoomIdx = (rooms.size() > 3) ? rooms.size() - 2 : 1;
|
||||
const auto& sr = rooms[shrineRoomIdx];
|
||||
int sx = sr.x + 1;
|
||||
int sy = sr.y + 1;
|
||||
if (dungeon.getTile(sx, sy) == TileType::FLOOR && (sx != res.startX || sy != res.startY) && (sx != res.exitX || sy != res.exitY)) {
|
||||
dungeon.addShrine(sx, sy);
|
||||
}
|
||||
}
|
||||
|
||||
// Identify corridor tiles for laser barriers and mines
|
||||
std::vector<std::pair<int, int>> vertCorridors;
|
||||
std::vector<std::pair<int, int>> horizCorridors;
|
||||
|
||||
for (int y = 2; y < H - 2; ++y) {
|
||||
for (int x = 2; x < W - 2; ++x) {
|
||||
if (dungeon.getTile(x, y) == TileType::FLOOR) {
|
||||
if ((x == res.startX && y == res.startY) || (x == res.exitX && y == res.exitY)) continue;
|
||||
if (dungeon.getShrineAt(x, y) || dungeon.getEnemyAt(x, y) || dungeon.getDoorAt(x, y) || dungeon.getLootAt(x, y)) continue;
|
||||
|
||||
bool wallLeft = (dungeon.getTile(x - 1, y) == TileType::WALL);
|
||||
bool wallRight = (dungeon.getTile(x + 1, y) == TileType::WALL);
|
||||
bool wallUp = (dungeon.getTile(x, y - 1) == TileType::WALL);
|
||||
bool wallDown = (dungeon.getTile(x, y + 1) == TileType::WALL);
|
||||
|
||||
if (wallLeft && wallRight && !wallUp && !wallDown) {
|
||||
vertCorridors.push_back({ x, y });
|
||||
} else if (wallUp && wallDown && !wallLeft && !wallRight) {
|
||||
horizCorridors.push_back({ x, y });
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Place 2-3 Laser barriers
|
||||
int lasersPlaced = 0;
|
||||
while (!vertCorridors.empty() && lasersPlaced < 2) {
|
||||
int idx = std::rand() % vertCorridors.size();
|
||||
auto pt = vertCorridors[idx];
|
||||
vertCorridors.erase(vertCorridors.begin() + idx);
|
||||
dungeon.addLaser(pt.first, pt.second, false);
|
||||
lasersPlaced++;
|
||||
}
|
||||
while (!horizCorridors.empty() && lasersPlaced < 3) {
|
||||
int idx = std::rand() % horizCorridors.size();
|
||||
auto pt = horizCorridors[idx];
|
||||
horizCorridors.erase(horizCorridors.begin() + idx);
|
||||
dungeon.addLaser(pt.first, pt.second, true);
|
||||
lasersPlaced++;
|
||||
}
|
||||
|
||||
// Place 2 Proximity Mines
|
||||
int minesPlaced = 0;
|
||||
std::vector<std::pair<int, int>> allCorridors = vertCorridors;
|
||||
allCorridors.insert(allCorridors.end(), horizCorridors.begin(), horizCorridors.end());
|
||||
while (!allCorridors.empty() && minesPlaced < 2) {
|
||||
int idx = std::rand() % allCorridors.size();
|
||||
auto pt = allCorridors[idx];
|
||||
allCorridors.erase(allCorridors.begin() + idx);
|
||||
if (!dungeon.getLaserAt(pt.first, pt.second)) {
|
||||
dungeon.addMine(pt.first, pt.second);
|
||||
minesPlaced++;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -27,6 +27,8 @@ void Player::reset(int startX, int startY, Direction startDir) {
|
|||
|
||||
m_revolver.reset();
|
||||
m_statuses.clear();
|
||||
m_implants.clear();
|
||||
m_defibrillatorUsed = false;
|
||||
|
||||
// Reset ammo pouch counts with a starter kit
|
||||
m_ammoCounts.fill(0);
|
||||
|
|
@ -233,4 +235,37 @@ int Player::getEffectiveDamage(AmmoType type) const {
|
|||
return getAmmoTierData(type, t).damage;
|
||||
}
|
||||
|
||||
bool Player::hasImplant(ImplantType type) const {
|
||||
for (auto imp : m_implants) {
|
||||
if (imp == type) return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
bool Player::addImplant(ImplantType type) {
|
||||
if (hasImplant(type)) return false;
|
||||
if (m_implants.size() >= MAX_IMPLANTS) return false;
|
||||
m_implants.push_back(type);
|
||||
return true;
|
||||
}
|
||||
|
||||
void Player::replaceImplant(size_t slotIndex, ImplantType type) {
|
||||
if (slotIndex < m_implants.size()) {
|
||||
m_implants[slotIndex] = type;
|
||||
}
|
||||
}
|
||||
|
||||
void Player::removeImplant(size_t slotIndex) {
|
||||
if (slotIndex < m_implants.size()) {
|
||||
m_implants.erase(m_implants.begin() + slotIndex);
|
||||
}
|
||||
}
|
||||
|
||||
void Player::setImplants(const std::vector<ImplantType>& implants) {
|
||||
m_implants = implants;
|
||||
if (m_implants.size() > MAX_IMPLANTS) {
|
||||
m_implants.resize(MAX_IMPLANTS);
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace dnr
|
||||
|
|
|
|||
|
|
@ -41,10 +41,12 @@ public:
|
|||
void addPicks(int amount) { m_cyberPicks += amount; }
|
||||
bool usePick();
|
||||
|
||||
int takeDamage(int amount, AmmoType element);
|
||||
void applyStatus(StatusEffect effect, int duration, int power);
|
||||
int takeDamage(int amount, AmmoType element = AmmoType::EMPTY);
|
||||
void applyStatus(StatusEffect effect, int duration, int power = 1);
|
||||
void rechargeShield(int amount) { restoreShield(amount); }
|
||||
void updateTurnStart(std::vector<std::string>& combatLog);
|
||||
bool isAlive() const { return m_hp > 0; }
|
||||
bool isDead() const { return m_hp <= 0; }
|
||||
|
||||
// Weapon & Ammo
|
||||
Revolver& getRevolver() { return m_revolver; }
|
||||
|
|
@ -82,6 +84,23 @@ public:
|
|||
|
||||
const std::vector<ActiveStatus>& getStatuses() const { return m_statuses; }
|
||||
|
||||
// Neural Implants (Max 4 slots)
|
||||
static constexpr size_t MAX_IMPLANTS = 4;
|
||||
const std::vector<ImplantType>& getImplants() const { return m_implants; }
|
||||
bool hasImplant(ImplantType type) const;
|
||||
bool addImplant(ImplantType type);
|
||||
void replaceImplant(size_t slotIndex, ImplantType type);
|
||||
void removeImplant(size_t slotIndex);
|
||||
void setImplants(const std::vector<ImplantType>& implants);
|
||||
void resetImplants() { m_implants.clear(); m_defibrillatorUsed = false; }
|
||||
|
||||
bool isDefibrillatorUsed() const { return m_defibrillatorUsed; }
|
||||
void setDefibrillatorUsed(bool used) { m_defibrillatorUsed = used; }
|
||||
void increaseMaxShield(int bonus) {
|
||||
m_maxShield += bonus;
|
||||
m_shield += bonus;
|
||||
}
|
||||
|
||||
private:
|
||||
int m_gridX = 1;
|
||||
int m_gridY = 1;
|
||||
|
|
@ -102,6 +121,8 @@ private:
|
|||
std::array<int, 7> m_ammoTiers{ 1, 1, 1, 1, 1, 1, 1 };
|
||||
Revolver m_revolver;
|
||||
std::vector<ActiveStatus> m_statuses;
|
||||
std::vector<ImplantType> m_implants;
|
||||
bool m_defibrillatorUsed = false;
|
||||
};
|
||||
|
||||
} // namespace dnr
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@ DbdSkillCheck::DbdSkillCheck() {
|
|||
init(1.0f);
|
||||
}
|
||||
|
||||
void DbdSkillCheck::init(float speedMultiplier) {
|
||||
void DbdSkillCheck::init(float speedMultiplier, float greatMultiplier) {
|
||||
m_needleAngle = 0.0f;
|
||||
m_speed = (180.0f + static_cast<float>(std::rand() % 40)) * speedMultiplier;
|
||||
|
||||
|
|
@ -17,8 +17,8 @@ void DbdSkillCheck::init(float speedMultiplier) {
|
|||
m_successStart = 120.0f + static_cast<float>(std::rand() % 100);
|
||||
m_successSpan = std::max(25.0f, 48.0f - (speedMultiplier - 1.0f) * 12.0f);
|
||||
|
||||
// Great success is at the end or center of the success span
|
||||
m_greatSpan = std::max(10.0f, m_successSpan * 0.32f);
|
||||
// Great success is at the end or center of the success span (expanded if greatMultiplier > 1)
|
||||
m_greatSpan = std::min(m_successSpan * 0.85f, std::max(10.0f, m_successSpan * 0.32f * greatMultiplier));
|
||||
m_greatStart = m_successStart + m_successSpan - m_greatSpan;
|
||||
|
||||
m_warningPlayed = false;
|
||||
|
|
|
|||
|
|
@ -15,7 +15,7 @@ class DbdSkillCheck {
|
|||
public:
|
||||
DbdSkillCheck();
|
||||
|
||||
void init(float speedMultiplier = 1.0f);
|
||||
void init(float speedMultiplier = 1.0f, float greatMultiplier = 1.0f);
|
||||
void update(float dt);
|
||||
|
||||
SkillCheckResult strike(); // Triggered on Space press
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ namespace dnr {
|
|||
|
||||
HackingGame::HackingGame() = default;
|
||||
|
||||
void HackingGame::start(int securityLevel) {
|
||||
void HackingGame::start(int securityLevel, bool expandedGreatZone) {
|
||||
m_securityLevel = securityLevel;
|
||||
m_phase = HackingPhase::PHASE_1_TUMBLER;
|
||||
m_resultTimer = 0.0f;
|
||||
|
|
@ -16,7 +16,18 @@ void HackingGame::start(int securityLevel) {
|
|||
m_tumbler.init(pinCount, difficulty);
|
||||
|
||||
float skillSpeed = 0.9f + securityLevel * 0.25f;
|
||||
m_skillCheck.init(skillSpeed);
|
||||
float greatMult = expandedGreatZone ? 2.0f : 1.0f;
|
||||
m_skillCheck.init(skillSpeed, greatMult);
|
||||
}
|
||||
|
||||
void HackingGame::startSkillCheckOnly(int securityLevel, bool expandedGreatZone) {
|
||||
m_securityLevel = securityLevel;
|
||||
m_phase = HackingPhase::PHASE_2_SKILLCHECK;
|
||||
m_resultTimer = 0.0f;
|
||||
|
||||
float skillSpeed = 0.85f + securityLevel * 0.20f;
|
||||
float greatMult = expandedGreatZone ? 2.0f : 1.0f;
|
||||
m_skillCheck.init(skillSpeed, greatMult);
|
||||
}
|
||||
|
||||
void HackingGame::update(float dt) {
|
||||
|
|
|
|||
|
|
@ -18,7 +18,8 @@ class HackingGame {
|
|||
public:
|
||||
HackingGame();
|
||||
|
||||
void start(int securityLevel);
|
||||
void start(int securityLevel, bool expandedGreatZone = false);
|
||||
void startSkillCheckOnly(int securityLevel, bool expandedGreatZone = false);
|
||||
void update(float dt);
|
||||
void handleInput();
|
||||
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
#include "InventoryUI.hpp"
|
||||
#include "render/UIRenderer.hpp"
|
||||
#include "core/SoundManager.hpp"
|
||||
#include <cmath>
|
||||
|
||||
|
|
@ -66,11 +67,11 @@ void InventoryUI::update(Player& player) {
|
|||
|
||||
// Ammo row mouse collision detection
|
||||
int pouchX = 660;
|
||||
int pouchY = 120;
|
||||
int pouchY = 105;
|
||||
int pouchW = 520;
|
||||
int startY = pouchY + 75;
|
||||
int startY = pouchY + 58;
|
||||
for (int i = 0; i < 6; ++i) {
|
||||
Rectangle rowRect = { static_cast<float>(pouchX + 15), static_cast<float>(startY + i * 50), static_cast<float>(pouchW - 30), 44.0f };
|
||||
Rectangle rowRect = { static_cast<float>(pouchX + 15), static_cast<float>(startY + i * 42), static_cast<float>(pouchW - 30), 38.0f };
|
||||
if (mouseClicked && CheckCollisionPointRec(mouse, rowRect)) {
|
||||
m_selectedTypeIndex = i;
|
||||
SoundManager::get().playTumblerClick();
|
||||
|
|
@ -117,65 +118,79 @@ void InventoryUI::draw(const Player& player) {
|
|||
Vector2 mouse = GetMousePosition();
|
||||
|
||||
// Dark semi-transparent cyber overlay
|
||||
DrawRectangle(0, 0, GetScreenWidth(), GetScreenHeight(), Color{ 4, 8, 16, 245 });
|
||||
DrawRectangle(0, 0, GetScreenWidth(), GetScreenHeight(), Color{ 5, 5, 5, 245 });
|
||||
|
||||
// Header
|
||||
DrawRectangle(80, 35, GetScreenWidth() - 160, 52, Color{ 12, 22, 38, 255 });
|
||||
DrawRectangleLines(80, 35, GetScreenWidth() - 160, 52, CyberColors::NeonCyan);
|
||||
DrawText("CYBER-WEAPON ARMORY // CYLINDER CONFIGURATION", 105, 50, 20, CyberColors::NeonCyan);
|
||||
DrawText("[TAB / ESC] RETURN TO GAME", GetScreenWidth() - 340, 51, 16, CyberColors::NeonYellow);
|
||||
DrawRectangle(80, 35, GetScreenWidth() - 160, 52, TacticalColors::DarkPanel);
|
||||
DrawRectangleLines(80, 35, GetScreenWidth() - 160, 52, TacticalColors::BorderWhite);
|
||||
DrawText("CYBER-WEAPON ARMORY // CYLINDER CONFIGURATION", 105, 50, 20, TacticalColors::PureWhite);
|
||||
DrawText("[TAB / ESC] RETURN TO GAME", GetScreenWidth() - 340, 51, 16, TacticalColors::Crimson);
|
||||
|
||||
// Left Panel: Revolver Cylinder
|
||||
// Left Panel: Revolver Cylinder (Strict 0-rounding)
|
||||
int cylCenterX = 350;
|
||||
int cylCenterY = 320;
|
||||
float cylRadius = 115.0f;
|
||||
float chamberOrbitR = cylRadius - 35.0f;
|
||||
float chamberR = 24.0f;
|
||||
|
||||
// 1. Heavy gunmetal outer housing
|
||||
DrawCircle(cylCenterX, cylCenterY, cylRadius + 18.0f, Color{ 8, 14, 24, 250 });
|
||||
DrawCircleLines(cylCenterX, cylCenterY, cylRadius + 18.0f, Color{ 30, 50, 75, 255 });
|
||||
|
||||
// Metallic cylinder body
|
||||
DrawCircle(cylCenterX, cylCenterY, cylRadius, Color{ 20, 28, 42, 255 });
|
||||
DrawCircleLines(cylCenterX, cylCenterY, cylRadius, CyberColors::NeonCyan);
|
||||
|
||||
// 6 Decorative flutes between chambers
|
||||
float chamberOrbitR = 58.0f;
|
||||
float visAngle = rev.getVisualAngle();
|
||||
for (int i = 0; i < 6; ++i) {
|
||||
float fluteAngle = (-visAngle + i * 60.0f + 30.0f - 90.0f) * (3.14159265f / 180.0f);
|
||||
int fluteX = cylCenterX + static_cast<int>(std::cos(fluteAngle) * (cylRadius + 2.0f));
|
||||
int fluteY = cylCenterY + static_cast<int>(std::sin(fluteAngle) * (cylRadius + 2.0f));
|
||||
DrawCircle(fluteX, fluteY, 14.0f, Color{ 8, 14, 24, 255 });
|
||||
DrawCircleLines(fluteX, fluteY, 14.0f, Color{ 40, 65, 95, 255 });
|
||||
}
|
||||
|
||||
// 2. Extractor Ratchet & Hub
|
||||
DrawCircle(cylCenterX, cylCenterY, 36.0f, Color{ 14, 20, 32, 255 });
|
||||
DrawCircleLines(cylCenterX, cylCenterY, 36.0f, Color{ 70, 100, 140, 255 });
|
||||
DrawCircle(cylCenterX, cylCenterY, 16.0f, Color{ 220, 175, 45, 255 });
|
||||
DrawCircleLines(cylCenterX, cylCenterY, 16.0f, CyberColors::NeonYellow);
|
||||
DrawText(".357", cylCenterX - 16, cylCenterY - 7, 13, CyberColors::NeonYellow);
|
||||
// 1. Faceted Octagonal Heavy Outer Housing
|
||||
DrawRectangle(cylCenterX - 110, cylCenterY - 110, 220, 220, TacticalColors::DarkPanel);
|
||||
DrawRectangleLines(cylCenterX - 110, cylCenterY - 110, 220, 220, TacticalColors::BorderMuted);
|
||||
|
||||
// 3. Golden Striker Hammer / Firing Pin at 12:00 (-90 deg)
|
||||
int strikerTipY = cylCenterY - static_cast<int>(cylRadius) - 4;
|
||||
int strikerBaseY = cylCenterY - static_cast<int>(cylRadius) - 28;
|
||||
// 45-degree chamfers on outer corners
|
||||
DrawTriangle({ (float)(cylCenterX - 110), (float)(cylCenterY - 110) },
|
||||
{ (float)(cylCenterX - 78), (float)(cylCenterY - 110) },
|
||||
{ (float)(cylCenterX - 110), (float)(cylCenterY - 78) }, TacticalColors::PitchBlack);
|
||||
DrawTriangle({ (float)(cylCenterX + 110), (float)(cylCenterY - 110) },
|
||||
{ (float)(cylCenterX + 78), (float)(cylCenterY - 110) },
|
||||
{ (float)(cylCenterX + 110), (float)(cylCenterY - 78) }, TacticalColors::PitchBlack);
|
||||
DrawTriangle({ (float)(cylCenterX - 110), (float)(cylCenterY + 110) },
|
||||
{ (float)(cylCenterX - 78), (float)(cylCenterY + 110) },
|
||||
{ (float)(cylCenterX - 110), (float)(cylCenterY + 78) }, TacticalColors::PitchBlack);
|
||||
DrawTriangle({ (float)(cylCenterX + 110), (float)(cylCenterY + 110) },
|
||||
{ (float)(cylCenterX + 78), (float)(cylCenterY + 110) },
|
||||
{ (float)(cylCenterX + 110), (float)(cylCenterY + 78) }, TacticalColors::PitchBlack);
|
||||
|
||||
DrawTriangle(
|
||||
{ static_cast<float>(cylCenterX), static_cast<float>(strikerTipY) },
|
||||
{ static_cast<float>(cylCenterX - 14), static_cast<float>(strikerBaseY) },
|
||||
{ static_cast<float>(cylCenterX + 14), static_cast<float>(strikerBaseY) },
|
||||
Color{ 255, 210, 50, 255 }
|
||||
);
|
||||
DrawRectangle(cylCenterX - 20, strikerBaseY - 14, 40, 14, Color{ 210, 160, 30, 255 });
|
||||
DrawRectangleLines(cylCenterX - 20, strikerBaseY - 14, 40, 14, CyberColors::NeonYellow);
|
||||
DrawCircle(cylCenterX, strikerTipY, 3.0f, CyberColors::NeonYellow);
|
||||
// 2. Inner Chamfered Drum Plate
|
||||
DrawRectangle(cylCenterX - 96, cylCenterY - 96, 192, 192, Color{ 16, 22, 32, 255 });
|
||||
DrawRectangleLines(cylCenterX - 96, cylCenterY - 96, 192, 192, TacticalColors::BorderWhite);
|
||||
DrawTriangle({ (float)(cylCenterX - 96), (float)(cylCenterY - 96) },
|
||||
{ (float)(cylCenterX - 70), (float)(cylCenterY - 96) },
|
||||
{ (float)(cylCenterX - 96), (float)(cylCenterY - 70) }, TacticalColors::DarkPanel);
|
||||
DrawTriangle({ (float)(cylCenterX + 96), (float)(cylCenterY - 96) },
|
||||
{ (float)(cylCenterX + 70), (float)(cylCenterY - 96) },
|
||||
{ (float)(cylCenterX + 96), (float)(cylCenterY - 70) }, TacticalColors::DarkPanel);
|
||||
DrawTriangle({ (float)(cylCenterX - 96), (float)(cylCenterY + 96) },
|
||||
{ (float)(cylCenterX - 70), (float)(cylCenterY + 96) },
|
||||
{ (float)(cylCenterX - 96), (float)(cylCenterY + 70) }, TacticalColors::DarkPanel);
|
||||
DrawTriangle({ (float)(cylCenterX + 96), (float)(cylCenterY + 96) },
|
||||
{ (float)(cylCenterX + 70), (float)(cylCenterY + 96) },
|
||||
{ (float)(cylCenterX + 96), (float)(cylCenterY + 70) }, TacticalColors::DarkPanel);
|
||||
|
||||
DrawRectangle(cylCenterX - 65, strikerBaseY - 32, 130, 16, Color{ 10, 16, 26, 230 });
|
||||
DrawRectangleLines(cylCenterX - 65, strikerBaseY - 32, 130, 16, Color{ 180, 150, 40, 200 });
|
||||
DrawText("FIRING PIN [12:00]", cylCenterX - 58, strikerBaseY - 30, 11, CyberColors::NeonYellow);
|
||||
// 3. Center Extractor Ratchet & Hub
|
||||
DrawRectangle(cylCenterX - 26, cylCenterY - 26, 52, 52, Color{ 12, 16, 24, 255 });
|
||||
DrawRectangleLines(cylCenterX - 26, cylCenterY - 26, 52, 52, TacticalColors::BorderMuted);
|
||||
DrawRectangle(cylCenterX - 16, cylCenterY - 16, 32, 32, TacticalColors::SurfacePlate);
|
||||
DrawRectangleLines(cylCenterX - 16, cylCenterY - 16, 32, 32, TacticalColors::Crimson);
|
||||
DrawText(".357", cylCenterX - 15, cylCenterY - 7, 13, TacticalColors::PureWhite);
|
||||
|
||||
// 4. Six Chambers (Rotates with rev.getVisualAngle())
|
||||
// 4. Aggressive Red Striker Firing Pin at 12:00 (-90 deg / top)
|
||||
int strikerW = 56;
|
||||
int strikerH = 16;
|
||||
int strikerX = cylCenterX - strikerW / 2;
|
||||
int strikerBaseY = cylCenterY - 110 - strikerH - 6;
|
||||
|
||||
DrawRectangle(strikerX, strikerBaseY, strikerW, strikerH, TacticalColors::DarkRed);
|
||||
DrawRectangleLines(strikerX, strikerBaseY, strikerW, strikerH, TacticalColors::BrightRed);
|
||||
DrawTriangle({ (float)cylCenterX, (float)(cylCenterY - 106) },
|
||||
{ (float)(cylCenterX - 9), (float)(strikerBaseY + strikerH) },
|
||||
{ (float)(cylCenterX + 9), (float)(strikerBaseY + strikerH) },
|
||||
TacticalColors::BrightRed);
|
||||
|
||||
const char* pinTxt = "[ 12:00 FIRING PIN ]";
|
||||
int pinW = MeasureText(pinTxt, 11);
|
||||
DrawText(pinTxt, cylCenterX - pinW / 2, strikerBaseY - 18, 11, TacticalColors::BrightRed);
|
||||
|
||||
// 5. Six Chambers (Rotates with rev.getVisualAngle())
|
||||
for (int i = 0; i < 6; ++i) {
|
||||
float angleRad = (-visAngle + i * 60.0f - 90.0f) * (3.14159265f / 180.0f);
|
||||
int chX = cylCenterX + static_cast<int>(std::cos(angleRad) * chamberOrbitR);
|
||||
|
|
@ -186,42 +201,45 @@ void InventoryUI::draw(const Player& player) {
|
|||
bool isSelected = (i == m_selectedChamber);
|
||||
bool isCurrentReady = (i == rev.getCurrentIndex());
|
||||
|
||||
DrawCircle(chX, chY, chamberR, Color{ 16, 22, 34, 255 });
|
||||
DrawCircleLines(chX, chY, chamberR, isSelected ? CyberColors::NeonYellow : Color{ 55, 80, 120, 255 });
|
||||
int chW = 36;
|
||||
int chH = 36;
|
||||
Rectangle chRect = { (float)(chX - chW / 2), (float)(chY - chH / 2), (float)chW, (float)chH };
|
||||
|
||||
if (ammo == AmmoType::EMPTY) {
|
||||
DrawCircle(chX, chY, chamberR - 4.0f, Color{ 8, 12, 18, 255 });
|
||||
DrawCircleLines(chX, chY, chamberR - 4.0f, Color{ 60, 60, 60, 255 });
|
||||
DrawText("0", chX - 4, chY - 6, 12, DARKGRAY);
|
||||
DrawRectangleRec(chRect, TacticalColors::PitchBlack);
|
||||
DrawRectangleLinesEx(chRect, isSelected ? 2.0f : 1.0f, isSelected ? TacticalColors::Crimson : (isCurrentReady ? TacticalColors::BorderWhite : TacticalColors::BorderMuted));
|
||||
DrawText("---", chX - 9, chY - 5, 10, TacticalColors::MutedText);
|
||||
} else {
|
||||
DrawCircle(chX, chY, chamberR - 4.0f, Color{ 34, 46, 62, 255 });
|
||||
DrawCircleLines(chX, chY, chamberR - 4.0f, Color{ 180, 140, 50, 255 });
|
||||
DrawCircle(chX, chY, chamberR - 8.0f, info.color);
|
||||
DrawCircle(chX - 4, chY - 4, 3.0f, Color{ 255, 255, 255, 200 });
|
||||
DrawText(info.shortName.substr(0, 3).c_str(), chX - 9, chY - 5, 11, BLACK);
|
||||
DrawRectangleRec(chRect, TacticalColors::DarkPanel);
|
||||
DrawRectangleLinesEx(chRect, isSelected ? 2.0f : 1.0f, isSelected ? TacticalColors::Crimson : info.color);
|
||||
// Tactical cartridge primer block
|
||||
DrawRectangle(chX - 7, chY - 7, 14, 14, Color{ info.color.r, info.color.g, info.color.b, 200 });
|
||||
DrawRectangleLines(chX - 7, chY - 7, 14, 14, info.color);
|
||||
DrawText(info.shortName.substr(0, 3).c_str(), chX - 10, chY - 5, 10, TacticalColors::PureWhite);
|
||||
}
|
||||
|
||||
// Selection Reticle
|
||||
// Selection Reticle (Square brackets)
|
||||
if (isSelected) {
|
||||
DrawRectangleLines(chX - static_cast<int>(chamberR) - 4, chY - static_cast<int>(chamberR) - 4,
|
||||
static_cast<int>(chamberR * 2) + 8, static_cast<int>(chamberR * 2) + 8,
|
||||
CyberColors::NeonYellow);
|
||||
DrawRectangleLinesEx({ chRect.x - 4, chRect.y - 4, chRect.width + 8, chRect.height + 8 }, 2.0f, TacticalColors::Crimson);
|
||||
DrawLine(chX, (int)chRect.y - 8, chX, (int)chRect.y - 3, TacticalColors::Crimson);
|
||||
DrawLine(chX, (int)(chRect.y + chRect.height + 3), chX, (int)(chRect.y + chRect.height + 8), TacticalColors::Crimson);
|
||||
DrawLine((int)chRect.x - 8, chY, (int)chRect.x - 3, chY, TacticalColors::Crimson);
|
||||
DrawLine((int)(chRect.x + chRect.width + 3), chY, (int)(chRect.x + chRect.width + 8), chY, TacticalColors::Crimson);
|
||||
}
|
||||
|
||||
// Active Chamber Marker (Under firing pin)
|
||||
if (isCurrentReady) {
|
||||
DrawCircleLines(chX, chY, chamberR + 6.0f, CyberColors::NeonGreen);
|
||||
DrawText("READY", chX - 16, chY + 28, 10, CyberColors::NeonGreen);
|
||||
DrawText("READY", chX - 16, chY + 24, 10, TacticalColors::PureWhite);
|
||||
}
|
||||
}
|
||||
|
||||
// Selected chamber info box below cylinder
|
||||
// Selected chamber info box below cylinder (Strict 0-rounding)
|
||||
int boxX = 110;
|
||||
int boxY = 480;
|
||||
int boxW = 480;
|
||||
int boxH = 100;
|
||||
DrawRectangle(boxX, boxY, boxW, boxH, Color{ 10, 18, 30, 255 });
|
||||
DrawRectangleLines(boxX, boxY, boxW, boxH, CyberColors::NeonCyan);
|
||||
DrawRectangle(boxX, boxY, boxW, boxH, TacticalColors::DarkPanel);
|
||||
DrawRectangleLines(boxX, boxY, boxW, boxH, TacticalColors::BorderMuted);
|
||||
|
||||
AmmoType selChamberAmmo = rev.getChamber(m_selectedChamber);
|
||||
const auto& selChamberInfo = getAmmoInfo(selChamberAmmo);
|
||||
|
|
@ -230,17 +248,17 @@ void InventoryUI::draw(const Player& player) {
|
|||
if (m_selectedChamber == rev.getCurrentIndex()) {
|
||||
selHeader += " [ACTIVE @ 12:00 FIRING PIN]";
|
||||
}
|
||||
DrawText(selHeader.c_str(), boxX + 15, boxY + 12, 15, CyberColors::NeonYellow);
|
||||
DrawText(selHeader.c_str(), boxX + 15, boxY + 12, 15, TacticalColors::Crimson);
|
||||
DrawText(("LOADED: " + selChamberInfo.name).c_str(), boxX + 15, boxY + 36, 15, selChamberInfo.color);
|
||||
DrawText(("IMPACT: " + std::to_string(selChamberInfo.baseDamage) + " DMG // " + selChamberInfo.description).c_str(), boxX + 15, boxY + 58, 12, RAYWHITE);
|
||||
DrawText("[1-6 / CLICK] SELECT | [U] UNLOAD | [R] SPIN ROULETTE", boxX + 15, boxY + 78, 11, CyberColors::NeonCyan);
|
||||
DrawText(("IMPACT: " + std::to_string(selChamberInfo.baseDamage) + " DMG // " + selChamberInfo.description).c_str(), boxX + 15, boxY + 58, 12, TacticalColors::SilverText);
|
||||
DrawText("[1-6 / CLICK] SELECT | [U] UNLOAD | [R] SPIN ROULETTE", boxX + 15, boxY + 78, 11, TacticalColors::MutedText);
|
||||
|
||||
// Ammo Probability Breakdown under info box
|
||||
int oddsY = boxY + boxH + 12;
|
||||
int oddsH = 65;
|
||||
DrawRectangle(boxX, oddsY, boxW, oddsH, Color{ 8, 14, 24, 255 });
|
||||
DrawRectangleLines(boxX, oddsY, boxW, oddsH, Color{ 35, 60, 95, 255 });
|
||||
DrawText("// AMMO PROBABILITY ROULETTE", boxX + 15, oddsY + 8, 10, GRAY);
|
||||
DrawRectangle(boxX, oddsY, boxW, oddsH, TacticalColors::DarkPanel);
|
||||
DrawRectangleLines(boxX, oddsY, boxW, oddsH, TacticalColors::BorderMuted);
|
||||
DrawText("// AMMO PROBABILITY ROULETTE", boxX + 15, oddsY + 8, 10, TacticalColors::MutedText);
|
||||
|
||||
int loadedCount = 0;
|
||||
int typeCounts[7] = { 0 };
|
||||
|
|
@ -253,10 +271,10 @@ void InventoryUI::draw(const Player& player) {
|
|||
int blankChance = 100 - hitChance;
|
||||
|
||||
std::string oddsStr = "LOADED: " + std::to_string(loadedCount) + "/6 (" + std::to_string(hitChance) + "% HIT)";
|
||||
DrawText(oddsStr.c_str(), boxX + 15, oddsY + 24, 13, hitChance >= 50 ? CyberColors::NeonGreen : CyberColors::NeonYellow);
|
||||
DrawText(oddsStr.c_str(), boxX + 15, oddsY + 24, 13, hitChance >= 50 ? TacticalColors::PureWhite : TacticalColors::Crimson);
|
||||
|
||||
std::string blankStr = "MISFIRE RISK: " + std::to_string(blankChance) + "%";
|
||||
DrawText(blankStr.c_str(), boxX + 220, oddsY + 24, 13, blankChance == 0 ? CyberColors::NeonGreen : (blankChance > 33 ? RED : CyberColors::NeonYellow));
|
||||
DrawText(blankStr.c_str(), boxX + 220, oddsY + 24, 13, blankChance == 0 ? TacticalColors::PureWhite : TacticalColors::Crimson);
|
||||
|
||||
std::string chipSummary = "";
|
||||
for (int t = 1; t <= 6; ++t) {
|
||||
|
|
@ -267,50 +285,57 @@ void InventoryUI::draw(const Player& player) {
|
|||
}
|
||||
}
|
||||
if (chipSummary.empty()) chipSummary = "ALL CHAMBERS EMPTY - LOAD CARTRIDGES FROM STACK";
|
||||
DrawText(chipSummary.c_str(), boxX + 15, oddsY + 44, 11, Color{ 170, 195, 225, 255 });
|
||||
DrawText(chipSummary.c_str(), boxX + 15, oddsY + 44, 11, TacticalColors::SilverText);
|
||||
|
||||
// Right Panel: Ammo Pouch / Storage (Stacked 6 Types)
|
||||
// Right Panel: Ammo Pouch / Storage + Neural Implants Matrix
|
||||
int pouchX = 660;
|
||||
int pouchY = 120;
|
||||
int pouchY = 105;
|
||||
int pouchW = 520;
|
||||
int pouchH = 550;
|
||||
int pouchH = 580;
|
||||
|
||||
DrawRectangle(pouchX, pouchY, pouchW, pouchH, Color{ 10, 16, 26, 255 });
|
||||
DrawRectangleLines(pouchX, pouchY, pouchW, pouchH, CyberColors::NeonCyan);
|
||||
DrawText("TACTICAL CARTRIDGE STACK // INVENTORY", pouchX + 20, pouchY + 15, 18, CyberColors::NeonCyan);
|
||||
DrawRectangle(pouchX, pouchY, pouchW, pouchH, TacticalColors::DarkPanel);
|
||||
DrawRectangleLines(pouchX, pouchY, pouchW, pouchH, TacticalColors::BorderWhite);
|
||||
DrawText("TACTICAL CARTRIDGE STACK // INVENTORY", pouchX + 20, pouchY + 12, 16, TacticalColors::PureWhite);
|
||||
|
||||
DrawText(("[W/S / CLICK] SELECT [ENTER / E / SPACE] LOAD INTO CHAMBER #" + std::to_string(m_selectedChamber + 1)).c_str(),
|
||||
pouchX + 20, pouchY + 45, 13, CyberColors::NeonYellow);
|
||||
pouchX + 20, pouchY + 36, 12, TacticalColors::Crimson);
|
||||
|
||||
int startY = pouchY + 75;
|
||||
int startY = pouchY + 58;
|
||||
for (int i = 0; i < 6; ++i) {
|
||||
AmmoType aType = AVAILABLE_TYPES[i];
|
||||
const auto& itemInfo = getAmmoInfo(aType);
|
||||
int count = player.getAmmoCount(aType);
|
||||
|
||||
bool isSel = (i == m_selectedTypeIndex);
|
||||
Rectangle rowRect = { static_cast<float>(pouchX + 15), static_cast<float>(startY + i * 50), static_cast<float>(pouchW - 30), 44.0f };
|
||||
Rectangle rowRect = { static_cast<float>(pouchX + 15), static_cast<float>(startY + i * 42), static_cast<float>(pouchW - 30), 38.0f };
|
||||
bool isHover = CheckCollisionPointRec(mouse, rowRect);
|
||||
|
||||
Color boxCol = isSel ? Color{ 30, 50, 80, 255 } : (isHover ? Color{ 20, 32, 52, 255 } : Color{ 14, 20, 32, 255 });
|
||||
Color borderCol = isSel ? CyberColors::NeonYellow : (isHover ? CyberColors::NeonCyan : Color{ 30, 50, 75, 255 });
|
||||
Color boxCol = isSel ? TacticalColors::SurfacePlate : (isHover ? Color{ 24, 24, 24, 255 } : TacticalColors::PitchBlack);
|
||||
Color borderCol = isSel ? TacticalColors::Crimson : (isHover ? TacticalColors::BorderWhite : TacticalColors::BorderMuted);
|
||||
|
||||
DrawRectangleRec(rowRect, boxCol);
|
||||
DrawRectangleLinesEx(rowRect, isSel ? 2.0f : 1.0f, borderCol);
|
||||
|
||||
// Icon
|
||||
DrawCircle(pouchX + 35, startY + i * 50 + 22, 11.0f, count > 0 ? itemInfo.color : DARKGRAY);
|
||||
// Tactical rectangular badge (Strict 0-rounding)
|
||||
DrawRectangle(pouchX + 22, startY + i * 42 + 9, 22, 20, count > 0 ? TacticalColors::SurfacePlate : TacticalColors::PitchBlack);
|
||||
DrawRectangleLines(pouchX + 22, startY + i * 42 + 9, 22, 20, count > 0 ? itemInfo.color : TacticalColors::BorderMuted);
|
||||
DrawText(itemInfo.shortName.substr(0, 3).c_str(), pouchX + 24, startY + i * 42 + 14, 9, count > 0 ? itemInfo.color : TacticalColors::MutedText);
|
||||
|
||||
// Name
|
||||
DrawText(itemInfo.name.c_str(), pouchX + 55, startY + i * 50 + 13, 15, count > 0 ? (isSel ? CyberColors::NeonYellow : RAYWHITE) : GRAY);
|
||||
DrawText(itemInfo.name.c_str(), pouchX + 50, startY + i * 42 + 11, 14, count > 0 ? (isSel ? TacticalColors::PureWhite : TacticalColors::SilverText) : TacticalColors::MutedText);
|
||||
|
||||
// Tier badge
|
||||
int tier = player.getAmmoTier(aType);
|
||||
std::string tStr = tier == 1 ? "T1" : (tier == 2 ? "T2" : "T3");
|
||||
DrawText(tStr.c_str(), pouchX + 230, startY + i * 42 + 12, 12, tier >= 3 ? TacticalColors::Crimson : TacticalColors::SilverText);
|
||||
|
||||
// Damage Tag
|
||||
DrawText(("DMG: " + std::to_string(itemInfo.baseDamage)).c_str(), pouchX + 230, startY + i * 50 + 13, 14, CyberColors::NeonCyan);
|
||||
DrawText(("DMG: " + std::to_string(itemInfo.baseDamage)).c_str(), pouchX + 270, startY + i * 42 + 12, 13, TacticalColors::PureWhite);
|
||||
|
||||
// Stack Count
|
||||
std::string countStr = "[x " + std::to_string(count) + "]";
|
||||
Color countColor = (count > 0) ? (isSel ? CyberColors::NeonYellow : CyberColors::NeonGreen) : RED;
|
||||
DrawText(countStr.c_str(), pouchX + 430, startY + i * 50 + 12, 16, countColor);
|
||||
Color countColor = (count > 0) ? (isSel ? TacticalColors::PureWhite : TacticalColors::SilverText) : TacticalColors::Crimson;
|
||||
DrawText(countStr.c_str(), pouchX + 430, startY + i * 42 + 11, 15, countColor);
|
||||
}
|
||||
|
||||
// Selected item detail footer
|
||||
|
|
@ -318,13 +343,72 @@ void InventoryUI::draw(const Player& player) {
|
|||
const auto& curSelInfo = getAmmoInfo(curSelType);
|
||||
int curSelCount = player.getAmmoCount(curSelType);
|
||||
|
||||
DrawRectangle(pouchX + 15, pouchY + pouchH - 120, pouchW - 30, 105, Color{ 14, 24, 40, 255 });
|
||||
DrawRectangleLines(pouchX + 15, pouchY + pouchH - 120, pouchW - 30, 105, CyberColors::NeonGreen);
|
||||
int detailY = startY + 6 * 42 + 4;
|
||||
int detailH = 64;
|
||||
DrawRectangle(pouchX + 15, detailY, pouchW - 30, detailH, TacticalColors::SurfacePlate);
|
||||
DrawRectangleLines(pouchX + 15, detailY, pouchW - 30, detailH, TacticalColors::BorderWhite);
|
||||
|
||||
DrawText(curSelInfo.name.c_str(), pouchX + 30, pouchY + pouchH - 110, 16, curSelInfo.color);
|
||||
DrawText(("IN STOCK: " + std::to_string(curSelCount) + " CARTRIDGES").c_str(), pouchX + 330, pouchY + pouchH - 110, 14, curSelCount > 0 ? CyberColors::NeonGreen : RED);
|
||||
DrawText(curSelInfo.description.c_str(), pouchX + 30, pouchY + pouchH - 85, 13, RAYWHITE);
|
||||
DrawText("[ENTER / E / SPACE] INSERT INTO SELECTED CHAMBER", pouchX + 30, pouchY + pouchH - 50, 13, CyberColors::NeonYellow);
|
||||
DrawText(curSelInfo.name.c_str(), pouchX + 26, detailY + 8, 14, curSelInfo.color);
|
||||
DrawText(("IN STOCK: " + std::to_string(curSelCount)).c_str(), pouchX + 370, detailY + 8, 12, curSelCount > 0 ? TacticalColors::PureWhite : TacticalColors::Crimson);
|
||||
UIRenderer::drawWrappedText(curSelInfo.description, pouchX + 26, detailY + 28, pouchW - 55, 11, 2, TacticalColors::SilverText);
|
||||
DrawText("[ENTER / E / SPACE] INSERT INTO SELECTED CHAMBER", pouchX + 26, detailY + 46, 11, TacticalColors::Crimson);
|
||||
|
||||
// Neural Implants Matrix Panel (2x2 grid)
|
||||
int impPanelY = detailY + detailH + 8;
|
||||
int impPanelH = pouchY + pouchH - impPanelY - 10;
|
||||
DrawRectangle(pouchX + 15, impPanelY, pouchW - 30, impPanelH, TacticalColors::DarkPanel);
|
||||
DrawRectangleLines(pouchX + 15, impPanelY, pouchW - 30, impPanelH, TacticalColors::BorderMuted);
|
||||
|
||||
std::string impHeader = "// NEURAL MATRIX RIG // SLOTS [" + std::to_string(player.getImplants().size()) + " / 4]";
|
||||
DrawText(impHeader.c_str(), pouchX + 25, impPanelY + 8, 12, TacticalColors::PureWhite);
|
||||
|
||||
std::string chipTxt = "[ SCRAP: " + std::to_string(player.getTechScraps()) + " ]";
|
||||
DrawText(chipTxt.c_str(), pouchX + pouchW - 120, impPanelY + 8, 12, TacticalColors::Crimson);
|
||||
|
||||
const auto& implants = player.getImplants();
|
||||
int slotW = 236;
|
||||
int slotH = 50;
|
||||
int startImpX = pouchX + 22;
|
||||
int startImpY = impPanelY + 28;
|
||||
|
||||
for (int s = 0; s < 4; ++s) {
|
||||
int col = s % 2;
|
||||
int row = s / 2;
|
||||
int sX = startImpX + col * (slotW + 8);
|
||||
int sY = startImpY + row * (slotH + 6);
|
||||
Rectangle sRect = { static_cast<float>(sX), static_cast<float>(sY), static_cast<float>(slotW), static_cast<float>(slotH) };
|
||||
bool isHoverImp = CheckCollisionPointRec(mouse, sRect);
|
||||
|
||||
if (s < static_cast<int>(implants.size())) {
|
||||
ImplantType imp = implants[s];
|
||||
const auto& data = getImplantData(imp);
|
||||
Color archCol = data.color;
|
||||
|
||||
if (imp == ImplantType::AUTO_DEFIBRILLATOR && player.isDefibrillatorUsed()) {
|
||||
archCol = TacticalColors::Crimson;
|
||||
}
|
||||
|
||||
DrawRectangleRec(sRect, TacticalColors::SurfacePlate);
|
||||
DrawRectangleLinesEx(sRect, isHoverImp ? 2.0f : 1.0f, isHoverImp ? TacticalColors::PureWhite : archCol);
|
||||
|
||||
std::string sTitle = "SLOT " + std::to_string(s + 1) + ": " + data.name;
|
||||
DrawText(sTitle.c_str(), sX + 8, sY + 6, 11, archCol);
|
||||
|
||||
if (imp == ImplantType::AUTO_DEFIBRILLATOR && player.isDefibrillatorUsed()) {
|
||||
DrawText("[STATUS: REVIVE DEPLETED]", sX + 8, sY + 24, 10, TacticalColors::Crimson);
|
||||
} else {
|
||||
std::string shortDesc = data.description;
|
||||
if (shortDesc.length() > 32) shortDesc = shortDesc.substr(0, 30) + "..";
|
||||
DrawText(shortDesc.c_str(), sX + 8, sY + 24, 10, TacticalColors::SilverText);
|
||||
}
|
||||
} else {
|
||||
DrawRectangleRec(sRect, TacticalColors::PitchBlack);
|
||||
DrawRectangleLinesEx(sRect, 1.0f, TacticalColors::BorderMuted);
|
||||
std::string emptyTitle = "SLOT " + std::to_string(s + 1) + ": [EMPTY SOCKET]";
|
||||
DrawText(emptyTitle.c_str(), sX + 8, sY + 8, 11, TacticalColors::MutedText);
|
||||
DrawText("Draft from Elites / ICE", sX + 8, sY + 26, 10, TacticalColors::BorderMuted);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace dnr
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
|
|
@ -32,7 +32,7 @@ private:
|
|||
void beginPostProcess();
|
||||
void endPostProcess(float animTime);
|
||||
|
||||
void renderCyberDrone(Vector3 pos, Color coreColor, float scale, float animTime, bool isHit = false, bool isAttacking = false);
|
||||
void renderCyberDrone(Vector3 pos, Color coreColor, float scale, float animTime, bool isHit = false, bool isAttacking = false, bool isElite = false);
|
||||
|
||||
Camera3D m_camera{};
|
||||
bool m_texturesLoaded = false;
|
||||
|
|
@ -63,6 +63,7 @@ private:
|
|||
Texture2D m_texMonitorRadar{};
|
||||
Texture2D m_texBedQuilt{};
|
||||
Texture2D m_texCityNight{};
|
||||
Texture2D m_texWindowView{};
|
||||
Texture2D m_texCyberGate{};
|
||||
|
||||
// Ancient Palace Storeroom Procedural Textures
|
||||
|
|
|
|||
144
src/render/ScreenTransition.hpp
Normal file
144
src/render/ScreenTransition.hpp
Normal file
|
|
@ -0,0 +1,144 @@
|
|||
#pragma once
|
||||
|
||||
#include "raylib.h"
|
||||
#include "core/Common.hpp"
|
||||
#include <functional>
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
|
||||
namespace dnr {
|
||||
|
||||
class ScreenTransition {
|
||||
public:
|
||||
enum class Phase {
|
||||
IDLE,
|
||||
WIPE_IN,
|
||||
HOLD_MIDPOINT,
|
||||
WIPE_OUT
|
||||
};
|
||||
|
||||
ScreenTransition() = default;
|
||||
|
||||
void start(std::function<void()> onMidpoint) {
|
||||
m_onMidpoint = onMidpoint;
|
||||
m_phase = Phase::WIPE_IN;
|
||||
m_timer = 0.0f;
|
||||
}
|
||||
|
||||
bool isBusy() const {
|
||||
return m_phase != Phase::IDLE;
|
||||
}
|
||||
|
||||
void update(float dt) {
|
||||
if (m_phase == Phase::IDLE) return;
|
||||
|
||||
m_timer += dt;
|
||||
|
||||
const float wipeInDuration = 0.35f;
|
||||
const float holdDuration = 0.05f;
|
||||
const float wipeOutDuration = 0.35f;
|
||||
|
||||
if (m_phase == Phase::WIPE_IN) {
|
||||
if (m_timer >= wipeInDuration) {
|
||||
m_phase = Phase::HOLD_MIDPOINT;
|
||||
m_timer = 0.0f;
|
||||
if (m_onMidpoint) {
|
||||
m_onMidpoint();
|
||||
}
|
||||
}
|
||||
} else if (m_phase == Phase::HOLD_MIDPOINT) {
|
||||
if (m_timer >= holdDuration) {
|
||||
m_phase = Phase::WIPE_OUT;
|
||||
m_timer = 0.0f;
|
||||
}
|
||||
} else if (m_phase == Phase::WIPE_OUT) {
|
||||
if (m_timer >= wipeOutDuration) {
|
||||
m_phase = Phase::IDLE;
|
||||
m_timer = 0.0f;
|
||||
m_onMidpoint = nullptr;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void draw() {
|
||||
if (m_phase == Phase::IDLE) return;
|
||||
|
||||
int screenW = GetScreenWidth();
|
||||
int screenH = GetScreenHeight();
|
||||
const int blockSize = 32;
|
||||
int cols = (screenW + blockSize - 1) / blockSize;
|
||||
int rows = (screenH + blockSize - 1) / blockSize;
|
||||
int maxDist = (cols - 1) + (rows - 1);
|
||||
if (maxDist <= 0) maxDist = 1;
|
||||
|
||||
if (m_phase == Phase::HOLD_MIDPOINT) {
|
||||
// Full opaque PitchBlack screen
|
||||
DrawRectangle(0, 0, screenW, screenH, TacticalColors::PitchBlack);
|
||||
return;
|
||||
}
|
||||
|
||||
const float wipeInDuration = 0.35f;
|
||||
const float wipeOutDuration = 0.35f;
|
||||
const float waveSpread = 0.32f;
|
||||
|
||||
bool isWipeIn = (m_phase == Phase::WIPE_IN);
|
||||
float progress = isWipeIn ? std::clamp(m_timer / wipeInDuration, 0.0f, 1.0f)
|
||||
: std::clamp(m_timer / wipeOutDuration, 0.0f, 1.0f);
|
||||
|
||||
for (int r = 0; r < rows; ++r) {
|
||||
for (int c = 0; c < cols; ++c) {
|
||||
float dist = static_cast<float>(c + r) / static_cast<float>(maxDist);
|
||||
float cellProgress = 0.0f;
|
||||
|
||||
if (isWipeIn) {
|
||||
// Blocks expand along wave: starts when progress >= dist * (1 - waveSpread)
|
||||
float startThreshold = dist * (1.0f - waveSpread);
|
||||
cellProgress = std::clamp((progress - startThreshold) / waveSpread, 0.0f, 1.0f);
|
||||
} else {
|
||||
// Blocks shrink along wave: dissolved as progress advances
|
||||
float startThreshold = dist * (1.0f - waveSpread);
|
||||
float dissolve = std::clamp((progress - startThreshold) / waveSpread, 0.0f, 1.0f);
|
||||
cellProgress = 1.0f - dissolve;
|
||||
}
|
||||
|
||||
if (cellProgress <= 0.001f) continue;
|
||||
|
||||
int posX = c * blockSize;
|
||||
int posY = r * blockSize;
|
||||
|
||||
if (cellProgress >= 0.999f) {
|
||||
// Full solid black block (Strict 0-rounding)
|
||||
DrawRectangle(posX, posY, blockSize, blockSize, TacticalColors::PitchBlack);
|
||||
} else {
|
||||
// Partial block growing / shrinking from center
|
||||
int curSize = static_cast<int>(blockSize * cellProgress);
|
||||
int offset = (blockSize - curSize) / 2;
|
||||
Rectangle bRect = {
|
||||
static_cast<float>(posX + offset),
|
||||
static_cast<float>(posY + offset),
|
||||
static_cast<float>(curSize),
|
||||
static_cast<float>(curSize)
|
||||
};
|
||||
|
||||
DrawRectangleRec(bRect, TacticalColors::PitchBlack);
|
||||
|
||||
// Tactical Crimson Edge on wave frontier
|
||||
DrawRectangleLinesEx(bRect, 1.0f, TacticalColors::Crimson);
|
||||
|
||||
// Glitch binary glyph on active frontline
|
||||
if (curSize > 14 && ((c * 7 + r * 13) % 4 == 0)) {
|
||||
const char* glyph = ((c + r) % 2 == 0) ? "1" : "0";
|
||||
DrawText(glyph, posX + blockSize / 2 - 3, posY + blockSize / 2 - 5, 10, TacticalColors::Crimson);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private:
|
||||
Phase m_phase = Phase::IDLE;
|
||||
float m_timer = 0.0f;
|
||||
std::function<void()> m_onMidpoint;
|
||||
};
|
||||
|
||||
} // namespace dnr
|
||||
File diff suppressed because it is too large
Load diff
|
|
@ -18,6 +18,16 @@ public:
|
|||
void drawWorkbenchModal(const Player& player);
|
||||
static Rectangle getWorkbenchUpgradeButtonRect(int ammoIndex, int screenW, int screenH);
|
||||
static Rectangle getWorkbenchCloseButtonRect(int screenW, int screenH);
|
||||
static void drawWrappedText(const std::string& text, int posX, int posY, int maxW, int fontSize, int lineSpacing, Color color);
|
||||
|
||||
void drawImplantDraftModal(const std::vector<ImplantType>& choices, const Player& player, int selectedReplaceSlot = -1);
|
||||
static Rectangle getDraftCardButtonRect(int cardIndex, int screenW, int screenH);
|
||||
static Rectangle getDraftScrapButtonRect(int screenW, int screenH);
|
||||
|
||||
void drawShrineModal(const Player& player);
|
||||
static Rectangle getShrineOptionButtonRect(int optionIndex, int screenW, int screenH);
|
||||
static Rectangle getShrineCloseButtonRect(int screenW, int screenH);
|
||||
|
||||
void drawHackingUI(const HackingGame& hacking);
|
||||
void drawCombatUI(const CombatSystem& combat, const Player& player, float animTime, bool showCodex = false);
|
||||
void drawCodexModal();
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue