ебеня ксанакса

инфо // инфо // инфо // инфо // инфо // инфо

I'm not afraid of the war you've come to wage against my sins
I'm not okay, but I can try my best to just pretend

какой-то текст описание проекта, очень познавательный, очень интересно какой-то текст описание проекта, очень познавательный, очень интересно какой-то текст описание проекта, очень познавательный, очень какой-то текст описание проекта, очень познавательный, очень интересно

design © damask.beresklet

Информация о пользователе

Привет, Гость! Войдите или зарегистрируйтесь.


Вы здесь » design © damask.beresklet » Тестовый форум » Тестовое сообщение


Тестовое сообщение

Сообщений 211 страница 226 из 226

211

https://upforme.ru/uploads/0017/24/ab/2/690377.gif
https://upforme.ru/uploads/0017/24/ab/2/826865.gif
https://upforme.ru/uploads/0017/24/ab/2/342757.gif
https://upforme.ru/uploads/0017/24/ab/2/335364.gif

0

212

[html]<link rel="stylesheet" type="text/css" href="https://forumstatic.ru/files/001c/b1/c6/12722.css"/>

<div class="lottery-grid" id="grid"></div>

<div id="lotteryModal">
  <div class="modal-content">
    <h3>Вы выиграли!</h3>
    <div id="rewardText" style="font-weight:bold;margin:10px 0;"></div>

    <button onclick="copyReward()">📋 Скопировать</button>
    <button onclick="closeModal()">Закрыть</button>
  </div>
</div>

<div id="toast"></div>

<script>
var rewards = [
  "100 монет",
  "50 монет",
  "VIP 1 день",
  "Редкий предмет",
  "Скин",
  "Бонус +10%"
];

function toast(msg){
  var t = document.getElementById("toast");
  t.innerHTML = msg;
  t.style.display = "block";
  setTimeout(function(){ t.style.display="none"; }, 1500);
}

window.onload = function(){
  var grid = document.getElementById("grid");
  if(!grid) return;

  for(var i=0;i<50;i++){
    var box = document.createElement("div");
    box.className = "box";

    box.onclick = function(){

      if(localStorage.getItem("lottery_used") === "1"){
        toast("❌ Уже использовано");
        return;
      }

      var reward = rewards[Math.floor(Math.random()*rewards.length)];

      document.getElementById("rewardText").innerHTML = reward;
      document.getElementById("lotteryModal").style.display = "flex";

      this.className = "box opened";

      localStorage.setItem("lottery_used","1");

      toast("🎁 Открыто!");
    };

    grid.appendChild(box);
  }
};

function closeModal(){
  document.getElementById("lotteryModal").style.display="none";
  toast("Закрыто");
}

function copyReward(){
  var text = document.getElementById("rewardText").innerText;

  var temp = document.createElement("textarea");
  document.body.appendChild(temp);
  temp.value = text;
  temp.select();
  document.execCommand("copy");
  document.body.removeChild(temp);

  toast("📋 Скопировано!");
}
</script>[/html]

0

213

https://upforme.ru/uploads/001c/5b/2f/414/968431.png

[html]<div style="width: 600px; margin-left: 55px; font-size: 8px; text-transform: lowercase; letter-spacing: 0.7px;"><a target="_blank" href="https://circuscross.rusff.me/profile.php?id=415" style="color: #ca9900; text-decoration: none!important; font-family: 'Deutsch Gothic'; font-size: 20px!important; float: left; padding: 6px;">самоуничтожитель</a> В масштабах вселенной жизнь ничтожно мала, и перед ликом Эонов люди лишь пыль. Если бы Эоны и впрямь были так могущественны, разве их интересовали бы человеческие страсти? По неосторожности попав в тень Эона Небытия IX, Самоуничтожители утратили смысл существования. Как тень Небытия накрывает случайную звезду, так и Самоуничтожители могут появиться в любом из миров. Их вряд ли можно назвать фракцией, поскольку между ними слишком мало социальных связей. Но если рассматривать их как некий феномен, то все эти несчастные души имеют и кое-что общее: разнообразные тела, сознание и воспоминания... И однажды они вымрут, следуя пути самоуничтожения. Это явление трудно объяснить с точки зрения законов природы. В записках Докторов Хаоса они описаны так: «Кожа некоторых из них превратилась в гниющий пергамент, испещрённый шрамами и язвами. Их эндокринная система дала сбой, и они перестали отличать радость от боли, проявляя ко всему равнодушие. Некоторые потеряли память, а у кого-то пострадали органы чувств. Кажется, будто у них отобрали смысл жизни. Им остаётся лишь наблюдать, как их силуэты исчезают в чёрной дыре за линией горизонта в бесконечных грёзах и иллюзиях».<br><br><a target="_blank" href="https://circuscross.rusff.me/profile.php?id=414" style="color: #ca9900; text-decoration: none!important; font-family: 'Deutsch Gothic'; font-size: 20px!important; float: right; padding: 6px;">опустошитель</a>  Лорд Опустошитель появляется в ответ на желание к уничтожению сущего и принимает командование одного из легионов. Лорд Опустошитель — эманатор Эона, Изъявитель воли Разрушения, который одержим красотой разрушения и стремится заставить всё вокруг подчиняться энтропии. Опустошители являются мастерами искусства войны и олицетворениями оружия абсолютного уничтожения. Они командуют межзвёздными битвами и контролируют силы Разрушения, дарованные им Эоном. У каждого Лорда Опустошителя своя собственная уникальная философия Разрушения, что делает их особенно опасными. Учёные полагают, что назначение Лордов Опустошителей посланниками Нанука определяется их невероятно глубокой и агрессивной теорией Разрушения. Хаос, вызванный Лордами Опустошителями — не чьи-то далёкие проблемы, они напрямую связаны с судьбой галактики. Любой мир может стать их следующей жертвой, и поэтому никому нельзя воспринимать их легкомысленно.</div>[/html]

Zephyro
ха-ха, я здесь живу
Zephyro
зефиро
все, что должно умереть
[само]разрушение

0

214

[14.05.2026 12:17] ксанакс: Skip to Content

Watch: Dungeon Masters Ep. 5 Airs Tonight at 6:30 PM PT

Dismiss

Play D&D
Rules
Library

Community
Marketplace
Sign inCreate an Account
Blood Hunter

Jump to...

Create your free Character
Blood Hunter Class Details
By Matt Mercer | Art by Joma Cueto

THIS IS PARTNERED CONTENT

This content is available in your campaign with your DM’s permission but isn’t published by Wizards of the Coast. To use this content, enable Critical Role in the character builder.

Marred but resolute, his grimacing face dripping with sweat, a half-orc reddens a finger in the blood of his own wounds, then draws a glowing ruby glyph in the air between him and the bloody behemoth facing him. He grips the weightless sigil, twisting it to unleash streams of dark energy that curse the blood of the monster’s own veins to even the odds.

A mysterious half-elf swathed in a worn cloak and rugged leather armor carefully investigates the site of a roadway massacre, her eyes flashing with recognition as she meditates on the remnants of the grisly scene. Suddenly, she jumps to her feet, certain in the knowledge of what creature was responsible, where it can be found—and how little time she has before it kills again.

Stepping into lightless chambers filled with ancient dust and lingering whispers, the halfling picks up the warning of imminent danger from the scraping of bone and claw on nearby stone. She winces as she runs her blade across her palm, the steel transmuting blood and essence into glowing runes of powerful magic, eager to brand and burn the flesh of her enemies.

Blood hunters are clever warriors driven by an unending determination to destroy evils old and new. Armed with rites of secretive blood magic and a willingness to sacrifice their own vitality and humanity for their cause, they protect the realms from the shadows—even as they remain ever vigilant against being drawn to the darkness that consumes the monsters they hunt.

Sacrifice to Preserve Life
Far from the judging eyes of society, blood hunters have mastered the secretive techniques of hemocraft, finding blood magic’s esoteric nature effective against evils that resist divine rebuke or arcane bindings. Through careful study and practice, blood hunters hone the rites of hemocraft into unique combat techniques, forfeiting a portion of their own health to call blood curses down upon their enemies or summon the elements to aid their strikes. Willing to suffer whatever it takes to achieve victory, these adept warriors have forged themselves into a potent force dedicated to protecting the innocent.

A Monster to Fight Monsters
Whether driven by the wish to make a difference, the need to take vengeance, or the hope of finding a place to belong in an uncaring world, every blood hunter has their own reasons for undertaking the ritual of the Hunter’s Bane that starts them on this path. In joining an order of blood hunters, one also joins a family bound by service to each other and a common cause. For many, this might be the only family they have left—or have ever known—making the kinship felt between blood hunters an all-but-unbreakable bond.

Outside the camaraderie of their orders, however, the life of a blood hunter is not an easy one. The ritual of the Hunter’s Bane can leave a character visibly changed, and prone to unsettling the people around them. Likewise, witnessing hemocraft can invoke superstitious fears from even the most learned scholars. While some cultures have come to accept the good deeds of many blood hunter orders, many blood hunters hide their calling unless absolutely necessary. They feel more comfortable in the wilds and wastes of the world, or drift through the outskirts of society, protecting the poor and defenseless from dark intention and the corrupting touch of fiends.
[14.05.2026 12:17] ксанакс: In choosing this path, every blood hunter irrevocably gives a part of themself to their cause—physically, emotionally, and sometimes morally. Each order of blood hunters practices its own ideals and methods, often employing techniques with dark origins that test the strength and will of those who employ them. Many wrestle with the fear of losing this struggle. And so a life of discipline and vigilance drives a blood hunter’s travels as they wander the countryside, in search of like-minded adventurers and whispers of dark deeds afoot.

Creating a Blood Hunter
As you create your blood hunter character, think about why you were driven to this lifestyle—and why you strive to give up everything to dwell in the darkness with the evils you hunt. Do you seek a sense of purpose and security, which you found among the order that has taken you in? Have you always carried a seed of darkness within you, so that you look for compatriots who can watch over and prevent you from succumbing to it? Were you once a holy warrior who strayed from your faith and was cast out, even as you yearn to give yourself over to the cause of protecting the innocent?

What is your relationship with the powers of hemocraft and the abilities it promises? Do you respect and fear the ancient power that surges through your veins, embracing your gifts and using them freely? Are you worried that this power will eventually turn you into one of the monsters you hunt? Or has your study instilled you with the confident clarity that makes you certain you can control these gifts for the greater good?

As well, what made you leave the comfort of your order to strike out on your own? Do you intend to return, or have you decided you have more to learn in the world? What strengths or assets do you seek in other adventurers that can help you meet your goals?

Though most blood hunters follow a path of good or neutrality in their pursuits, some have fallen to the dark, seductive side of hemocraft. These blood hunters use their abilities for selfish and evil purposes, often leading to their expulsion from the orders that trained them.

Quick Build

You can make a blood hunter quickly by following these suggestions. First, put your highest ability score in Strength or Dexterity, depending on whether you want to focus on melee weapons or on archery (or finesse weapons). Your next-highest score should be Intelligence, if you plan to focus on the potency of blood curses and mystical power, or Constitution, if you want to have additional hit points to empower your abilities through sacrifice. Second, choose the soldier or urchin background.

The Blood Hunter
Level

Proficiency
Bonus

Hemocraft
Die

Features Known

Blood Curses

1st

+2

1d4

Hunter's Bane, Blood Maledict

1

2nd

+2

1d4

Fighting Style, Crimson Rite

1

3rd

+2

1d4

Blood Hunter Order

1

4th

+2

1d4

Ability Score Improvement

1

5th

+3

1d6

Extra Attack

1

6th

+3

1d6

Brand of Castigation, Blood Maledict improvement

2

7th

+3

1d6

Blood Hunter Order feature, Crimson Rite improvement

2

8th

+3

1d6

Ability Score Improvement

2

9th

+4

1d6

Grim Psychometry

2

10th

+4

1d6

Dark Augmentation

3

11th

+4

1d8

Blood Hunter Order feature

3

12th

+4

1d8

Ability Score Improvement

3

13th

+5

1d8

Brand of Tethering, Blood Maledict improvement

3

14th

+5

1d8

Hardened Soul, Crimson Rite improvement

4

15th

+5

1d8

Blood Hunter Order feature

4

16th

+5

1d8

Ability Score Improvement

4

17th

+6

1d10

Blood Maledict improvement

4

18th

+6

1d10

Blood Hunter Order feature

5

19th

+6

1d10

Ability Score Improvement

5

20th

+6

1d10

Sanguine Mastery

5

Optional Rule: Multiclassing
If your group uses the multiclassing rules from the Player’s Handbook, here’s what you need to know if you choose blood hunter as one of your classes.

Ability Score Minimum. As a multiclass character, you must have at least a Strength score or Dexterity score of 13 and an Intelligence score of 13 to take a level in this class, or to take a level in another class if you are already a blood hunter.
[14.05.2026 12:17] ксанакс: Proficiencies Gained. If blood hunter isn’t your initial class, you gain the following proficiencies when you take your first level as a blood hunter: light armor, medium armor, shields, simple weapons, martial weapons, and alchemist’s supplies.

Multiclassing with Warlock. If your blood hunter is part of the Order of the Profane Soul and also has warlock levels, add one-third of your blood hunter levels (rounded down) to your warlock level and consult the warlock progression table in the Player’s Handbook for total spell slots, cantrips known, and spell slot level. You should consider aligning your Otherworldly Patron feature between both classes, but your DM might allow you to have two different patrons at their discretion.

Variant Hemocraft Ability Score

As a blood hunter, you use your Intelligence modifier for some of your class and subclass features. However, with your DM’s permission, you can choose to instead use your Wisdom modifier for all your blood hunter features that use your Intelligence modifier by default.

Hit Points
Hit Dice: 1d10 per blood hunter level
Hit Points at 1st Level: 10 + your Constitution modifier
Hit Points at Higher Levels: 1d10 (or 6) + your Constitution modifier per blood hunter level after 1st

Proficiencies
Armor: Light armor, medium armor, shields
Weapons: Simple weapons, martial weapons
Tools: Alchemist’s supplies
Saving Throws: Dexterity, Intelligence
Skills: Choose three from Acrobatics, Arcana, Athletics, History, Insight, Investigation, Religion, and Survival

Equipment
You start with the following equipment, in addition to the equipment granted by your background:

a martial weapon or two simple weapons
a light crossbow and 20 bolts
studded leather armor or scale mail armor
an explorer’s pack and alchemist’s supplies
Hunter’s Bane
At 1st level, you have survived the Hunter’s Bane—a dangerous, long-guarded ritual that alters your life’s blood, forever binding you to the darkness and honing your senses against it. You have advantage on Wisdom (Survival) checks to track fey, fiends, or undead, as well as on Intelligence checks to recall information about such creatures.

The Hunter’s Bane also empowers your body to control and shape hemocraft magic, using your own blood and life essence to fuel your abilities. Some of your features require your target to make a saving throw to resist the feature’s effects. The saving throw DC is calculated as follows:

Hemocraft save DC = 8 + your proficiency bonus + your Hemocraft modifier
(your choice between Intelligence or Wisdom)

Blood Maledict
Also at 1st level, you gain the ability to channel—or sometimes sacrifice—a part of your vital essence to curse and manipulate creatures through hemocraft magic. You know one blood curse of your choice, detailed in the “Blood Curses” section at the end of the class description. You learn one additional blood curse of your choice at 6th, 10th, 14th, and 18th level. Each time you learn a new blood curse, you can also choose one of the blood curses you know and replace it with another blood curse.

Each time you use your Blood Maledict feature, you choose which curse to invoke from the curses you know. While invoking a blood curse, but before it affects the target, you can choose to amplify the curse by taking necrotic damage equal to one roll of your hemocraft die. This damage can’t be reduced in any way. An amplified curse gains an additional effect, noted in the curse’s description. Creatures that do not have blood are immune to blood curses unless you have amplified the curse.

Once you use this feature, you must finish a short or long rest before you can use it again. You can use Blood Maledict twice between rests starting at 6th level, three times starting at 13th level, and four times starting at 17th level.

Fighting Style
At 2nd level, you adopt a style of fighting as your specialty. Choose one of the following options. You can’t take a Fighting Style option more than once, even if you later get to choose again.

Archery
You gain a +2 bonus to attack rolls you make with ranged weapons.
[14.05.2026 12:17] ксанакс: Dueling
When you are wielding a melee weapon in one hand and no other weapons, you gain a +2 bonus to damage rolls with that weapon.

Great Weapon Fighting
When you roll a 1 or 2 on a damage die for an attack you make with a melee weapon that you are wielding with two hands, you can reroll the die and must use the new roll. The weapon must have the two-handed or versatile property for you to gain this benefit.

Two-Weapon Fighting
When you engage in two-weapon fighting, you can add your ability modifier to the damage of the second attack.

Crimson Rite
Also at 2nd level, you learn to invoke a rite of hemocraft that infuses your weapon strikes with elemental energy. As a bonus action, you can activate any rite you know on one weapon you’re holding. The effect of the rite lasts until you finish a short or long rest. When you activate a rite, you take necrotic damage equal to one roll of your hemocraft die. This damage can’t be reduced in any way.

While the rite is in effect, attacks you make with this weapon are magical, and deal extra damage equal to your hemocraft die of the type determined by the chosen rite. A weapon can hold only one active rite at a time. Other creatures can’t gain the benefit of your rite.

You choose one rite from the crimson rites below when you first gain this feature. You learn an additional crimson rite at 7th level, and again at 14th level.

Rite of the Flame. The extra damage dealt by your rite is fire damage.

Rite of the Frozen. The extra damage dealt by your rite is cold damage.

Rite of the Storm. The extra damage dealt by your rite is lightning damage.

Rite of the Dead. The extra damage dealt by your rite is necrotic damage. (Prerequisite: 14th level)

Rite of the Oracle. The extra damage dealt by your rite is psychic damage. (Prerequisite: 14th level)

Rite of the Roar. The extra damage dealt by your rite is thunder damage. (Prerequisite: 14th level)

Blood Hunter Order
At 3rd level, you commit to an order of blood hunters whose philosophy will guide you throughout your life: the Order of the Ghostslayer, the Order of the Lycan, the Order of the Mutant, or the Order of the Profane Soul, each of which is detailed at the end of the class description. Your choice grants you features at 7th level and again at 11th, 15th, and 18th level.

Ability Score Improvement
When you reach 4th level, and again at 8th, 12th, 16th and 19th level, you can increase one ability score of your choice by 2, or you can increase two ability scores of your choice by 1. As normal, you can’t increase an ability score above 20 using this feature.

Using the optional feats rule, you can forgo taking this feature to take a feat of your choice instead.

Extra Attack
Starting at 5th level, you can attack twice, instead of once, whenever you take the Attack action on your turn.

Brand of Castigation
At 6th level, when you damage a creature with a weapon for which you have an active crimson rite, you can channel hemocraft magic to sear an arcane brand into that creature (no action required). You always know the direction to the branded creature as long as it’s on the same plane as you. Further, each time the branded creature deals damage to you or a creature you can see within 5 feet of you, the branded creature takes psychic damage equal to your Hemocraft modifier (minimum of 1).

Your brand lasts until you dismiss it or until you use this feature to apply a brand to another creature. Your brand can be dispelled with dispel magic, and is treated as a spell with a level equal to half your blood hunter level (maximum 9th level).

Once you use this feature, you can’t use it again until you finish a short or long rest.

Grim Psychometry
When you reach 9th level, you gain a supernatural talent for discerning the secrets surrounding mysterious relics or places touched by evil. Whenever you make an Intelligence (History) check to recall information about the sinister or tragic history of an object you are touching or your current location, you have advantage on the check.
[14.05.2026 12:17] ксанакс: At the DM’s discretion, a suitably high roll might cause your character to experience brief visions of the past connected to the object or location.

Dark Augmentation
Starting at 10th level, the magic of hemocraft suffuses your body to permanently reinforce your resilience. Your speed increases by 5 feet, and you have a bonus to Strength, Dexterity, and Constitution saving throws equal to your Hemocraft modifier (minimum of +1).

Brand of Tethering
Starting at 13th level, the psychic damage from your Brand of Castigation increases to twice your Hemocraft modifier (minimum of 2). Additionally, a branded creature can’t take the Dash action, and if it attempts to teleport or to leave its current plane by any means, it takes 4d6 psychic damage and must make a Wisdom saving throw. On a failure, the attempt to teleport or leave the plane fails.

Hardened Soul
When you reach 14th level, you have advantage on saving throws against being charmed and frightened.

Sanguine Mastery
Upon reaching 20th level, your mastery of blood magic reaches its height, mitigating your sacrifice and empowering your expertise. Once per turn, whenever a blood hunter feature requires you to roll a hemocraft die, you can reroll the die and use either roll.

Additionally, whenever you score a critical hit with a weapon for which you have an active crimson rite, you regain one expended use of your Blood Maledict feature.

Blood Curses
As a blood hunter, you have access to a range of blood curses that can tax the resilience of any foe.

Blood Curse of the Anxious
As a bonus action, you harry the body or mind of a creature within 30 feet of you, making them susceptible to forceful influence. Until the end of your next turn, Charisma (Intimidation) checks made against the cursed creature have advantage.

Amplify. The next Wisdom saving throw the cursed creature makes before this curse ends has disadvantage.

Blood Curse of Binding
As a bonus action, you attempt to bind a Large or smaller creature you can see within 30 feet of you, which must make a Strength saving throw. On a failure, the cursed creature’s speed is reduced to 0 and it can’t use reactions until the end of your next turn.

Amplify. This curse lasts for 1 minute and can affect any creature regardless of size. The cursed creature can repeat the saving throw at the end of each of its turns, ending the curse on itself on a success.

Blood Curse of Bloated Agony
As a bonus action, you curse a creature that you can see within 30 feet of you, causing its body to swell until the end of your next turn. For the duration, the creature has disadvantage on Strength checks and Dexterity checks, and takes 1d8 necrotic damage if it makes more than one attack during its turn.

Amplify. This curse lasts for 1 minute. The cursed creature can make a Constitution saving throw at the end of each of its turns, ending the curse on itself on a success.

Blood Curse of Corrosion
Prerequisite: 15th level, Order of the Mutant

As a bonus action, you cause a creature within 30 feet of you to become poisoned. The cursed creature can make a Constitution saving throw at the end of each of its turns, ending the curse on itself on a success.

Amplify. The cursed creature takes 4d6 necrotic damage when you inflict this curse, and it takes this damage again each time it fails a Constitution saving throw to end the curse.

Blood Curse of the Exorcist
Prerequisite: 15th level, Order of the Ghostslayer

As a bonus action, you choose one creature you can see within 30 feet of you that is charmed or frightened, or which is under a possession effect. The target creature is no longer charmed, frightened, or possessed.

Amplify. A creature that charmed, frightened, or possessed the target of your curse takes 3d6 psychic damage and must succeed on a Wisdom saving throw or be stunned until the end of your next turn.
[14.05.2026 12:17] ксанакс: Blood Curse of Exposure
When a creature you can see within 30 feet of you takes damage from an attack or spell, you can use your reaction to temporarily weaken its resilience. Until the end of the target’s next turn, it loses resistance to all the damage types dealt by the triggering attack or spell (including for that triggering effect).

Amplify. The target instead loses invulnerability to the damage types of the triggering attack or spell, but has resistance to those damage types until the end of its next turn.

Blood Curse of the Eyeless
When a creature you can see within 30 feet of you makes an attack, you can use your reaction to roll one hemocraft die and subtract the number rolled from the creature’s attack roll. You can choose to use this feature after the creature’s roll, but before the DM determines whether the attack hits or misses. The creature is immune to this curse if it is immune to the blinded condition.

Amplify. You apply this curse to all the creature’s attack rolls until the end of the creature’s turn. You roll separately for each affected attack.

Blood Curse of the Fallen Puppet
When a creature you can see within 30 feet of you drops to 0 hit points, you can use your reaction to instill that creature with a final act of aggression. The creature immediately makes one weapon attack against a target of your choice within its range.

Amplify. You can first cause the cursed creature to move up to half its speed, and you grant a bonus to its attack roll equal to your Hemocraft modifier (minimum of +1).

Blood Curse of the Howl
Prerequisite: 18th level, Order of the Lycan

As an action, you unleash a bloodcurdling howl. Each creature within 30 feet of you that can hear you must succeed on a Wisdom saving throw or become frightened of you until the end of your next turn. If a creature fails its saving throw by 5 or more, it is stunned while frightened in this way. A creature that succeeds on its saving throw is immune to this blood curse for the next 24 hours.

You can choose any number of creatures you can see to be unaffected by the howl.

Amplify. The range of this curse increases to 60 feet.

Blood Curse of the Marked
As a bonus action, you mark a creature that you can see within 30 feet of you. Until the end of your turn, whenever you hit the cursed creature with a weapon for which you have an active crimson rite, you roll an additional hemocraft die when determining the extra damage from the rite.

Amplify. The next attack roll you make against the target before the end of your turn has advantage.

Blood Curse of the Muddled Mind
As a bonus action, you curse a creature that you can see within 30 feet of you that is concentrating on a spell or using a feature that requires concentration. That creature has disadvantage on the next Constitution saving throw it makes to maintain concentration before the end of your next turn.

Amplify. The cursed creature has disadvantage on all Constitution saving throws made to maintain concentration until the end of your next turn.

Blood Curse of the Soul Eater
Prerequisite: 18th level, Order of the Profane Soul

When a creature that isn’t a construct or undead is reduced to 0 hit points within 30 feet of you, you can use your reaction to offer their life energy to your patron in exchange for power. Until the end of your next turn, you make attacks with advantage and you have resistance to all damage.

Amplify. Additionally, you regain an expended warlock spell slot. Once you’ve amplified this blood curse, you must finish a long rest before you can amplify it again.

Blood Hunter Orders
A handful of secretive orders shape and define the knowledge of the blood hunters, their members all guarding unique arrays of cryptic techniques and rituals. Characters must seek out one of these orders to even be granted access to the Hunter’s Bane rite that starts each blood hunter’s journey. But only once a blood hunter has proven their dedication and worth will an order’s most powerful secrets be revealed.
[14.05.2026 12:17] ксанакс: Order of the Ghostslayer
The Order of the Ghostslayer is the oldest of the blood hunter orders, its members having originally rediscovered the secrets of hemocraft and refined them for combat against the scourge of undeath. Ghostslayers seek out and study the moment of death, obsessing over the mystery of the transition from life, and the unholy power that can cause the dead to rise once more. These zealous blood hunters make it their life’s work to destroy the scourge of undeath wherever it is found, tuning their abilities to engage undead creatures and those who manipulate the necromancy that creates them.

Rite of the Dawn
When you join this order at 3rd level, you learn the Rite of the Dawn as part of your Crimson Rite feature. When you activate the Rite of the Dawn, the extra damage dealt by your rite is radiant damage. Additionally, while that rite is active on your weapon, you gain the following benefits:

Your weapon sheds bright light out to a range of 20 feet.
You have resistance to necrotic damage.
When you hit an undead creature with a weapon for which the Rite of the Dawn is active, you roll an additional hemocraft die when determining the extra damage from the rite.
Curse Specialist
Starting at 3rd level, you learn to master blood curses. You gain an additional use of your Blood Maledict feature. In addition, your blood curses can target any creature, whether it has blood or not.

Aether Walk
Upon reaching 7th level, at the start of your turn, you can magically step into the veil between the planes as long as you aren’t incapacitated. You can move through other creatures and objects as if they were difficult terrain, as well as see and affect creatures and objects on the Ethereal Plane. You take 1d10 force damage if you end your turn inside an object.

This feature lasts for a number of rounds equal to your Hemocraft modifier (minimum of 1 round). If you are inside an object when it ends, you are immediately shunted to the nearest unoccupied space and you take force damage equal to twice the number of feet you moved.

Once you use this feature, you must finish a short or long rest before you can use it again. You can use Aether Walk twice between rests starting at 15th level.

Brand of Sundering
Starting at 11th level, your Brand of Castigation exposes a fragment of your foe’s essence, leaving them vulnerable to your Crimson Rite feature. Whenever you hit a creature with a weapon for which you have an active crimson rite, you roll an additional hemocraft die when determining the extra damage from the rite. Additionally, if a branded creature has the Incorporeal Movement trait or a similar feature, it can’t move through creatures or objects while branded.

Blood Curse of the Exorcist
At 15th level, you hone your hemocraft to tear corruption from the minds and bodies of your allies—and to punish those responsible for it. You gain the Blood Curse of the Exorcist for your Blood Maledict feature. This doesn’t count against your number of blood curses known.

Rite Revival
Upon reaching 18th level, you learn to protect your fading life by reabsorbing the energy you feed to your weapons. If you have one or more crimson rites active and you are reduced to 0 hit points but don’t die outright, you can choose to have all your active crimson rites end and drop to 1 hit point instead.

Order of the Lycan
The ancient curse of lycanthropy is feared by nearly all peoples and cultures, passed through blood and seeding a host with the savage strength and hunger for violence of a wicked beast. The Order of the Lycan is a proud group of blood hunters who undergo “the Taming”—the ceremonial infliction of lycanthropy by a senior member of the order, for those who do not already carry the curse before seeking this path. These hunters then use the magic of their blood to harness the power of the monster they harbor, without losing themselves to it. Using intense will and secret blood magic rituals, members of the Order of the Lycan learn to control and unleash their hybrid forms for short periods of time.
[14.05.2026 12:17] ксанакс: Enhanced physical prowess, unnatural resilience, and razor-sharp claws make these warriors a terrible foe to any evil that crosses their path. Yet no training is perfect, and without care and complete focus, even the greatest of blood hunters can temporarily lose themselves to their own hunger.

The Burden of Lycanthropy

Those inducted into the Order of the Lycan choose this path with conviction, understanding the terrible weight it imposes on them and the challenges it brings. Where most afflicted by the curse of lycanthropy grow wicked, deranged, and even murderous, Lycan blood hunters accept the gifts of the beast while maintaining control through intense training and the power of their blood magic. A member of the Order of the Lycan cannot spread their curse through blood unless they wish to, and one of the most sacred oaths of this order is to never infect another creature without the order’s sanction.

If a member of the Order of the Lycan is ever cured of their lycanthropic curse, it brings terrible shame to their name and the order. Members who have been cleansed against their will readily return to the order to undergo a renewed initiation of the Taming, reintroducing the curse to their bodies and restoring their honor.

Lycanthropy comes in many forms bound to specific beasts, with wolf, bear, tiger, boar, and rat the best-known variations. The particular strain of the lycanthropic curse defines the physical traits that manifest in a Lycan blood hunter’s hybrid transformation, even as the benefits the curse bestows remain relatively uniform.

Heightened Senses
When you choose this archetype at 3rd level, you gain the improved senses of a natural predator. You have advantage on Wisdom (Perception) checks that rely on hearing or smell.

Hybrid Transformation
Also at 3rd level, you learn to control the lycanthropic curse that courses through your veins. As a bonus action, you transform into a special hybrid form for up to 1 hour. You can speak, use equipment, and wear armor while in this form, and can revert to your normal form as a bonus action. You automatically revert to your normal form if you fall unconscious or die.

This feature replaces the rules for lycanthropy in the Monster Manual. Once you use this feature, you must finish a short or long rest before you can use it again.

In the Character Builder, set the option for Hybrid Transformation to Hybrid Form to activate the bonuses for that form on the character sheet.

Hybrid Transformation Features
While you are transformed, you gain the following benefits and drawbacks:

Feral Might. You have advantage on Strength checks and Strength saving throws, and you have a +1 bonus to melee damage rolls. This bonus increases to +2 at 11th level and to +3 at 18th level.

Resilient Hide. You have resistance to bludgeoning, piercing, and slashing damage from nonmagical attacks not made with silvered weapons. Additionally, while you are not wearing heavy armor, you have a +1 bonus to AC.

Predatory Strikes. You can apply your Crimson Rite feature to your unarmed strikes, which you treat as one weapon. You can use Dexterity instead of Strength for the attack and damage rolls of your unarmed strikes, which deal 1d6 bludgeoning or slashing damage (your choice). This damage increases to 1d8 at 11th level.

Additionally, when you use the Attack action to make an unarmed strike, you can make one additional unarmed strike as a bonus action.

Bloodlust. If you start your turn with fewer hit points than half your hit point maximum, you must succeed on a DC 8 Wisdom saving throw or move directly toward the nearest creature and use the Attack action against that creature. If you’re concentrating on a spell or are under an effect that prevents you from concentrating (such as the barbarian’s Rage feature), you automatically fail this saving throw.
[14.05.2026 12:17] ксанакс: If you have your Extra Attack feature, you can choose whether to use it for this frenzied attack. If more than one creature is equally near to you, roll randomly to determine your target. Once your attack is resolved, you regain control of yourself.

Stalker's Prowess
At 7th level, your speed increases by 10 feet, and you add 10 feet to your long jump distance and 3 feet to your high jump distance. Your hybrid form also gains the following additional benefit.

Improved Predatory Strikes. You have a +1 bonus to attack rolls made with your unarmed strike. This bonus increases to +2 at 11th level and to +3 at 18th level. Additionally, when you have an active crimson rite on your unarmed strike while in your hybrid form, your unarmed strikes are considered magical for the purpose of overcoming resistance and immunity to nonmagical attacks and damage.

Advanced Transformation
At 11th level, you learn to unleash and control more of the beast within. You can use your Hybrid Transformation feature twice, regaining all expended uses when you finish a short or long rest. Your hybrid form also gains the following additional benefit.

Lycan Regeneration. At the start of each of your turns when you have at least 1 hit point but fewer hit points than half your hit point maximum, you gain hit points equal to 1 + your Constitution modifier (minimum of 1). If you are in hybrid form, you gain these hit points before you must make the saving throw for your bloodlust.

Brand of the Voracious
Starting at 15th level, you have advantage on the saving throw for your bloodlust while in hybrid form. Additionally, your Brand of Castigation can now bind a foe to your hunter’s ferocity. While in your hybrid form, you have advantage on attack rolls against a creature branded by you.

Hybrid Transformation Mastery
At 18th level, you have mastered your inner predator. You can use your Hybrid Transformation feature an unlimited number of times, and your hybrid form lasts until you revert to your normal form, fall unconscious, or die.

You also gain the Blood Curse of the Howl for your Blood Maledict feature. This doesn’t count against your number of blood curses known.

Order of the Mutant
The process of undertaking the Hunter’s Bane ritual is a painful, scarring, and sometimes fatal experience. Those who survive are irrevocably changed—and not always for the better. Over generations of experimentation, a splinter order of blood hunters honed the way in which hemocraft alters the body, using corrupted alchemy and toxic elixirs to alter their blood even further. Over time, they have modified their capabilities in battle, becoming something beyond what they once were. Calling themselves the Order of the Mutant, these blood hunters now specialize in assessing the strengths and weaknesses of their foes, altering their biology to be best prepared for any conflict.

Mutagencraft
When you choose this archetype at 3rd level, you learn to master forbidden alchemical formulas—known as mutagens—that can temporarily alter your mental and physical abilities.

As a bonus action, you consume a mutagen, whose effects and side effects last until you finish a short or long rest unless otherwise specified. While one or more mutagens are affecting you, you can use an action to focus and flush all mutagens from your system, ending their effects and side effects.

Mutagens are designed for the specific biology of the character who concocted them, and your mutagens have no effect on other creatures. They are also unstable by nature, losing their potency over time and becoming inert if not used before you finish your next short or long rest.

Mutagencraft

Blood Hunter
Level Mutagens
Created Formulas
Known
3rd 1 4
7th 2 5
11th 2 6
15th 3 7
18th 3 8
Formulas
The number of mutagens you can concoct when you finish a rest, and the number of formulas you know, increases as you gain levels in the blood hunter class, as shown on the Mutagencraft table above. Additionally, when you learn a new mutagen formula, you can replace one formula you already know with a new mutagen formula.
[14.05.2026 12:17] ксанакс: You choose four mutagen formulas to learn from the options detailed at the end of this subclass description, and you can concoct one mutagen when you finish a short or long rest.

Strange Metabolism
When you reach 7th level, your body begins to adapt to toxins and venoms, ignoring their corrupting effects. You gain immunity to poison damage and the poisoned condition.

Additionally, you can trigger a burst of adrenaline that lets you temporarily resist the negative effects of a mutagen. As a bonus action, you can ignore the negative side effect of one mutagen affecting you for 1 minute. Once you do so, you can’t do so again until you finish a long rest.

Brand of Axiom
At 11th level, your mutagenic hemocraft lets your Brand of Castigation reveal a foe’s true nature. Any illusion or invisibility in effect on a creature when you brand it ends, and the creature can’t benefit from invisibility or illusion effects while branded by you. If a creature branded by you is in an alternative form (by way of the polymorph spell, the Change Shape action or Shapechanger trait, the Wild Shape feature, and similar effects), it must succeed on a Wisdom saving throw or revert to its true form and be stunned until the end of your next turn. Whenever a branded creature attempts to alter its form, it must succeed on a Wisdom saving throw or have the attempt fail, and it is stunned until the end of your next turn.

Blood Curse of Corrosion
Starting at 15th level, your blood curse can infuse a creature’s body with terrible toxins. You gain the Blood Curse of Corrosion for your Blood Maledict feature. This doesn’t count against your number of blood curses known.

Exalted Mutation
At 18th level, your body has adapted to produce mutagens naturally in a moment of need. As a bonus action, choose one mutagen currently affecting you. Its effects and side effects end, and you can immediately have a mutagen you know the formula for take effect in its place.

You can use this feature a number of times equal to your Hemocraft modifier (minimum of once). You regain all expended uses when you finish a long rest.

Mutagens
The mutagens that are part of your hemocraft are presented in alphabetical order. You can learn a mutagen at the same time you meet its prerequisites.

Aether
Prerequisite: 11th level

You have a flying speed of 20 feet for 1 hour. However, you have disadvantage on Strength checks and Dexterity checks during this time.

Alluring
Your skin and voice become malleable, allowing you to enhance your appearance and presence. You have advantage on Charisma checks. However, you have disadvantage on initiative rolls.

Celerity
Your Dexterity score increases by 3, as does your maximum for that score. However, you have disadvantage on Wisdom saving throws. Your Dexterity score and your maximum increase by 4 if you consume this mutagen at 11th level, and by 5 at 18th level.

Conversant
You have advantage on Intelligence checks. However, you have disadvantage on Wisdom checks.

Cruelty
Prerequisite: 11th level

When you use the Attack action, you can make one additional weapon attack as a bonus action. However, you have disadvantage on Intelligence, Wisdom, and Charisma saving throws.

Deftness
You have advantage on Dexterity checks. However, you have disadvantage on Wisdom checks.

Embers
You have resistance to fire damage and vulnerability to cold damage.

Gelid
You have resistance to cold damage and vulnerability to fire damage.

Impermeable
You have resistance to piercing damage and vulnerability to slashing damage.

Mobility
You have immunity to the grappled and restrained conditions. However, you have disadvantage on Strength checks. At 11th level, you are also immune to the paralyzed condition.

Nighteye
You have darkvision out to a range of 60 feet. If you already have darkvision, its range increases by 60 feet. However, you have disadvantage on attack rolls and on Wisdom (Perception) checks that rely on sight when you, the target of your attack, or whatever you are trying to perceive is in direct sunlight.
[14.05.2026 12:17] ксанакс: Percipient
You have advantage on Wisdom checks. However, you have disadvantage on Charisma checks.

Potency
Your Strength score increases by 3, as does your maximum for that score. However, you have disadvantage on Dexterity saving throws. Your Strength score and your maximum increase by 4 if you consume this mutagen at 11th level, and by 5 at 18th level.

Precision
Prerequisite: 11th level

Your weapon attacks score a critical hit on a roll of 19 or 20. However, you have disadvantage on Strength saving throws.

Rapidity
Your speed increases by 10 feet. However, you have disadvantage on Intelligence checks. At 15th level, your speed increases by an additional 5 feet.

Reconstruction
Prerequisite: 7th level

For 1 hour, at the start of each of your turns when you have at least 1 hit point but fewer hit points than half your hit point maximum, you regain hit points equal to your proficiency bonus. However, your speed is reduced by 10 feet during this time.

Sagacity
Your Intelligence score increases by 3, as does your maximum for that score. However, you have disadvantage on Charisma saving throws. Your Intelligence score and your maximum increase by 4 if you consume this mutagen at 11th level, and by 5 at 18th level.

Shielded
You have resistance to slashing damage, and you have vulnerability to bludgeoning damage.

Unbreakable
You have resistance to bludgeoning damage, and you have vulnerability to piercing damage.

Vermillion
You gain an additional use of your Blood Maledict feature. However, you have disadvantage on death saving throws.

Order of the Profane Soul
Blood hunters belonging to the Order of the Profane Soul have pushed the limits of hemocraft for use against some of the most terrifying creatures corrupting the world. Ancient fiends and cruel magic-users have long counted on their ability to meld into the background and escape those who hunt them, vanishing into noble courts without a trace, or bending the minds of the most stalwart warriors with but a glance. So the blood hunters who founded this order trusted to their resilience as they delved into the same well of corrupting arcane knowledge, making pacts with lesser evils to better combat the greater threats. And though they might have traded a part of themselves for their power, the members of this order know the benefits of that power far outweigh the price.

Otherworldly Patron
When you reach 3rd level, you strike a bargain with an otherworldly being of your choice:

The Archfey, the Fiend, or the Great Old One, detailed in the Player’s Handbook
The Undying, from Sword Coast Adventurer’s Guide
The Celestial or the Hexblade, from Xanathar’s Guide to Everything
The Fathomless or the Genie, from Tasha’s Cauldron of Everything
The Undead, from Van Richten’s Guide to Ravenloft
The choice you make augments some of your subclass features, as noted below.

Pact Magic
Also at 3rd level, you augment your combat techniques with the ability to cast spells. See chapter 10 of the Player’s Handbook for the general rules of spellcasting and chapter 11 of the Player’s Handbook for the warlock spell list.

Cantrips. You learn two cantrips of your choice from the warlock spell list. You learn an additional warlock cantrip of your choice at 10th level.

Spell Slots. The Profane Soul Spellcasting table shows how many spell slots you have. The table also shows what the level of those slots is; all of your spell slots are the same level. To cast one of your warlock spells of 1st level or higher, you must expend a spell slot. You regain all expended spell slots when you finish a short or long rest.

For example, when you are 8th level, you have two 2nd-level spell slots. To cast the 1st-level spell witch bolt, you must spend one of those slots, and you cast it as a 2nd-level spell.

Spells Known of 1st Level and Higher. At 3rd level, you know two 1st-level spells of your choice from the warlock spell list.
[14.05.2026 12:17] ксанакс: The Spells Known column of the Profane Soul Spellcasting table shows when you learn more warlock spells of your choice of 1st level and higher. A spell you choose must be of a level no higher than what’s shown in the table’s Slot Level column for your level. When you reach 13th level, for example, you learn a new warlock spell, which can be 1st, 2nd, or 3rd level.

Additionally, when you gain a level in this class, you can choose one of the warlock spells you know and replace it with another spell from the warlock spell list, which also must be of a level for which you have spell slots.

Spellcasting Ability. Your chosen Hemocraft ability (Intelligence or Wisdom) is your spellcasting ability for your warlock spells, so you use your Hemocraft ability whenever a spell refers to your spellcasting ability. In addition, you use your Hemocraft modifier when setting the saving throw DC for a warlock spell you cast and when making an attack roll with one.

Spell save DC = 8 + your proficiency bonus + your Hemocraft modifier

Spell attack modifier = your proficiency bonus + your Hemocraft modifier

Profane Soul Spellcasting
BLOOD HUNTER
LEVEL CANTRIPS
KNOWN SPELLS
KNOWN SPELL
SLOTS SLOT
LEVEL
3rd 2 2 1 1st
4th 2 2 1 1st
5th 2 3 1 1st
6th 2 3 2 1st
7th 2 4 2 2nd
8th 2 4 2 2nd
9th 2 5 2 2nd
10th 3 5 2 2nd
11th 3 6 2 2nd
12th 3 6 2 2nd
13th 3 7 2 3rd
14th 3 7 2 3rd
15th 3 8 2 3rd
16th 3 8 2 3rd
17th 3 9 2 3rd
18th 3 9 2 3rd
19th 3 10 2 4th
20th 3 11 2 4th
Rite Focus
Starting at 3rd level, your weapon becomes a conduit for the power of your pact with your chosen patron. While you have an active crimson rite, you can use your weapon as a spellcasting focus for your warlock spells, and you gain a specific benefit based on your chosen pact.

The Archfey. When you damage a creature with a weapon for which you have an active crimson rite, that creature glows with faint light until the end of your next turn. For the duration, the creature gains no benefit from any cover or from being invisible.

The Celestial. As a bonus action, you expend one use of your Blood Maledict feature to heal one creature you can see within 60 feet of you. That creature regains a number of hit points equal to one roll of your hemocraft die + your Hemocraft modifier (minimum of +1).

The Fathomless. You can breathe underwater. Additionally, once per turn when you damage a creature with a weapon for which you have an active crimson rite, you can reduce that creature’s speed by 10 feet until the start of your next turn.

The Fiend. While using the Rite of the Flame, if you roll a 1 or 2 on a damage die when you roll the extra damage from the rite, you can reroll the die and choose which roll to use.

The Genie. As a bonus action, you expend a use of your Blood Maledict feature to give yourself a flying speed of 30 feet, which lasts for a number of rounds equal to your Hemocraft modifier (minimum of 1 round).

The Great Old One. When you score a critical hit against a creature, that creature and any other creatures of your choice within 10 feet of it are frightened of you until the end of your next turn.

The Hexblade. When you successfully target a creature with a blood curse, the next time you hit that creature with an attack while the curse is in effect, the attack deals additional damage equal to your proficiency modifier.

The Undead. When you take necrotic damage, you can use your reaction to halve that damage. In addition, your appearance changes to reflect some aspect of your patron while you have any crimson rite active.

The Undying. When you reduce a hostile creature of at least mild threat (DM’s discretion) to 0 hit points, you regain a number of hit points equal to one roll of your hemocraft die.

Mystic Frenzy
Starting at 7th level, when you use your action to cast a cantrip, you can immediately make one weapon attack as a bonus action.

Revealed Arcana
At 7th level, your patron grants you the use of a distinctive spell based on your pact. You cast this spell using any pact magic spell slot, and can’t do so again until you finish a long rest.
[14.05.2026 12:17] ксанакс: The Archfey. You can cast blur.

The Celestial. You can cast lesser restoration.

The Fathomless. You can cast gust of wind.

The Fiend. You can cast scorching ray.

The Genie. You can cast phantasmal force.

The Great Old One. You can cast detect thoughts.

The Hexblade. You can cast branding smite.

The Undead. You can cast blindness/deafness.

The Undying. You can cast silence.

Brand of the Sapping Scar
Upon reaching 11th level, your Brand of Castigation feature digs dark arcane scars into your target, leaving them vulnerable to your magic. A creature branded by you has disadvantage on saving throws against your warlock spells.

Unsealed Arcana
At 15th level, your patron grants you the use of an additional spell based on your pact. You cast this spell without expending a spell slot, and can’t do so again until you finish a long rest.

The Archfey. You can cast slow.

The Celestial. You can cast revivify.

The Fathomless. You can cast lightning bolt.

The Fiend. You can cast fireball.

The Genie. You can cast protection from energy.

The Great Old One. You can cast haste.

The Hexblade. You can cast blink.

The Undead. You can cast speak with dead.

The Undying. You can cast bestow curse.

Blood Curse of the Souleater
Starting at 18th level, you learn to siphon the life energy from your fallen prey. You gain the Blood Curse of the Soul Eater for your Blood Maledict feature. This doesn’t count against your number of blood curses known.

Comments
lefty_the_wind_adept's avatar
lefty_the_wind_adeptPosted May 3, 2026
This reminds me of the Witcher almost like the profane soul is specifically for them....

iheartdritzzt67's avatar
iheartdritzzt67Posted Apr 9, 2026
um im a walrus

bugkiller's avatar
bugkillerPosted Mar 5, 2026
2024 rules NOW!!!!!!!

theaxehander39's avatar
theaxehander39Posted Feb 7, 2026
Blood hunter’s are bad ass

RevanStormwind's avatar
RevanStormwindPosted Feb 2, 2026
Quote from hzzb4djqvq >>
I agree potato66807. But they didn’t think about that when they made the wizard or Artifacter. ☠️ but yeah I love blood hunter, also what are all of your favorite races to play as a blood hunter mine is Human

mine is Reborn. technically a lineage not a race, but meh. tomato-tomato

Last edited by RevanStormwind: Feb 2, 2026
RevanStormwind's avatar
RevanStormwindPosted Feb 2, 2026
I abso-fricking-lutely LOVE this class! I used it to make my Revenant character!

jharpe09's avatar
jharpe09Posted Jan 29, 2026
update for 2024 rules

Lazyspoontwo's avatar
LazyspoontwoPosted Jan 11, 2026
I want to see this class get added to basic rule books, and also see the class get more buffs.

the_isatope8's avatar
the_isatope8Posted Dec 29, 2025
I would genuinely love to see this class get revised to fit more in line with the 2024 classes. It would be great to see some of its abilities get streamlined and have new features like the weapon masteries get added to its kit. Maybe we can finally get a second class with a 1d12 hit die?

darknessfighter's avatar
darknessfighterPosted Dec 29, 2025
I hope that they update this class soon.

sagetheprimorbial's avatar
sagetheprimorbialPosted Dec 22, 2025
definintley

tinyfey's avatar
tinyfeyPosted Dec 14, 2025
Quote from Chaosrocks >>
does any one else think that blood hunters are bad ass?.

Absolutely.

Vanitas_42's avatar
Vanitas_42Posted Dec 5, 2025
probably not but it can still be plugged into anything like all other 2014 content can be. the only major changes in 2024 are mostly wording or additions that make things more powerful.

Vanitas_42's avatar
Vanitas_42Posted Dec 5, 2025
Quote from moonglow89 >>
Can we get this in Beyond app, PLEASE?

It has been. for years. Just select Partnered Content and make sure Critical Role is turned on. Works in the app as well.

moonglow89's avatar
moonglow89Posted Nov 27, 2025
Can we get this in Beyond app, PLEASE?
[14.05.2026 12:17] ксанакс: Marandahir's avatar
MarandahirPosted Nov 2, 2025
Quote from Zachrael >>
Blood Hunter would work better as just being a subclass for any of the classes it relates to but doesn't actually take up the subclass slot if your Blood Hunter Order and Otherworldly Patron feats are related to your class. So like, if you were a Warlock you could still have two patrons with these changes. Like, yes, Order of The Lycan is basically "What if Barbarian but furry?". So if you're a Barbarian you get Order of The Lycan for free. If you're a Paladin or Cleric you get Ghostslayer for free. Warlock, Profaned Soul Order and Otherworldy Patron for free. There's really not anything related to a ranger or a rogue besides the "Marks" stuff but meh. and the Mutagen Order is more like a knock off of the Artificer subclass Alchemist, not Artificer itself. and I'd even say in exchange for you actual subclass you could take on one of the other Orders.

I’d just call myself a Blood Hunter and use one if the related classes’ appropriate subclass, much as Swordmages do with their related classes:

Alchemist or Reanimator; Path of the Beast or Path of the Zealot; Arcana, Grave, or Twilight Domain with Protector Order; Circle of the Moon or Circle of Spores with Warden Order; Warrior of Mercy, of Shadow, or of Long Death (or now also Warrior of Intoxication given its similarity to the Mutagenists); Oathbreaker, Oath of Conquest, Oath of the Watchers, Oath of Vengeance; Gloom Stalker, Hollow Warden, or Monster Slayer; Phantom or Scion of the Three; Defiled or Shadow Magic Sorcery; Fiend, Hexblade, or Undead Patron; Bladesinger or Necromancer.

Last edited by Marandahir: Nov 2, 2025
hzzb4djqvq's avatar
hzzb4djqvqPosted Oct 14, 2025
And treedy could you make a subclass that is combat based and like the battle master and champion and still has the lycan form but better?

hzzb4djqvq's avatar
hzzb4djqvqPosted Oct 14, 2025
I agree potato66807. But they didn’t think about that when they made the wizard or Artifacter. ☠️ but yeah I love blood hunter, also what are all of your favorite races to play as a blood hunter mine is Human

Treedy11's avatar
Treedy11Posted Oct 13, 2025
5 words, anti-far realm blood hunter

order of the plane guard

Far Understanding
When you choose this archetype at 3rd level, you gain advantage on all throw agents being charmed or frightened by a aberration

Plane Guard
Also at 3rd level, you learn to control the life energy in living things, giving you the ability to deal 2d8(4d8 at level 11) necrotic damage to any creature within 30 ft 3 times per long rest, and heal 2d8(4d8 at level 11) damage to any creature within 30 ft 3 times per long rest

Alien form
At 7th level, your body starts to warp, you gain a blindsight of 20 feet as eye stalk grow from your head, you also gain a tentacle, that gives you advantage on Destraity throws.

Power Of The Far
At 7th level, you can now cast arms of Hadar, hunger of Hadar, and Evard's black tentacles at will

Warp Mind
At 11th level, your power over life energy now extends to the mind, 2 times per day you can cast dominate person or monster with no save

Travel Reality
At 15th level, you gain the ability to teleport 120 feet in any direction, and you can teleport a creature other then your self

Blood Of The Eternal
Starting at 18th level, your power over life energy is so great you no longer age

Potato66807's avatar
Potato66807Posted Oct 9, 2025
The subclasses are not weak!

Ghostslayer's Rite of the Dawn makes you a mighty undead slayer, and undead enemies are common in D&D, plus Aether Walk is just plain fun.

Lycan allows you to become a ferocious and durable tank mid-combat! So what if Barbarians can too? Can a Barbarian cast Blood Curses? No. Can they cast spells while in tank mode (rage)? No. Can a Lycan? Yes.

Mutant gives immunity to poison at 7th level which is spectacular, and there are a few AMAZING mutagens in there like Reconstruction, Aether, and Vermillion. Just pick those.
[14.05.2026 12:17] ксанакс: Profane Soul is a lot like Eldritch Knights and Arcane Tricksters in that the magic is just a little extra something to bolster your power and your utility outside of combat. It's not the main attraction. You're not a wizard, so don't expect to have as much magical skill as one. I like Hexblade and Fiend for this one, because the extra damage is nice (as are the spells!)

Also if these subclasses seem weak or useless to you, it's because Blood Hunters aren't as reliant on subclass features as other classes, such as Monks and Fighters. Blood Hunters already have so many amazing abilities, so if the subclasses were too powerful, Blood Hunters would be the most powerful class ever. It's a balance issue.

Last edited by Potato66807: Oct 9, 2025
To post a comment, please login or register a new account.
1
2
3
4
5

62
Next

Our Terms of Service and Privacy Notice have recently been updated to provide greater clarity as to how disputes are handled and transparency regarding the collection and use of personal data. Please review them here: Terms of Service, Privacy Notice. By continuing to use the services, you agree to the new Terms.
D&D Beyond
Support
Help Portal
Support Forum
System Status
Do Not Sell or Share My Personal Information
Your Privacy Choices
Cookie Notice
System Reference Document (SRD)
About
Contact Us
Careers
Wizards of the Coast
Find us on social media
D&D Discord
D&D Beyond Instagram
D&D Beyond Facebook
D&D Beyond Twitch
D&D Beyond X
D&D Beyond YouTube
TikTok
Download the D&D Beyond App
Google Play
App Store
© 2017-2026 Wizards of the Coast LLC | All Rights Reserved
Dungeons & Dragons, D&D Beyond, D&D, Wizards of the Coast, the dragon ampersand, and all other Wizards of the Coast product names, campaign settings, their respective logos, and The World's Greatest Roleplaying Game are © and trademark Wizards of the Coast in the U.S.A. and other countries. © 2026 Wizards.
Privacy Policy
Terms of Service
ESRB

0

215

[html]<!DOCTYPE html>
<html lang="ru">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=yes">
    <title>fire worshipper · плейлист персонажа</title>
    <style>
        /* ---------- ОСНОВНОЙ СТИЛЬ: серые оттенки, градиент только вверху, без теней и анимации движения ---------- */
        * {
            margin: 0;
            padding: 0;
            box-sizing: border-box;
        }

        body {
            background: #0B0C10;
            display: flex;
            align-items: center;
            justify-content: center;
            min-height: 100vh;
            font-family: 'Inter', -apple-system, 'Segoe UI', 'SF Pro Text', 'Poppins', system-ui, sans-serif;
            padding: 2rem;
        }

        .forum-post {
            max-width: 980px;
            width: 100%;
            margin: 0 auto;
            margin-top: 3rem;
        }

        .playlist-card {
            background: #0F1111;  /* сплошной тёмный фон для всей карточки */
            border-radius: 32px;
            overflow: hidden;
            border: 1px solid rgba(255, 255, 255, 0.06);
            /* тень полностью убрана */
            box-shadow: none;
            /* анимация движения убрана */
            transition: none;
        }

        /* градиентная обёртка только для верхней части (аватар + заголовки) */
        .hero-gradient-bg {
            background: linear-gradient(180deg, #343434 0%, #0F1111 100%);
            border-radius: 32px 32px 0 0;
            padding: 2rem 2rem 1rem 2rem;
            margin: -2rem -2rem 0 -2rem;
        }

        /* компенсация внутренних отступов */
        .playlist-inner {
            padding: 2rem 2rem 2rem 2rem;
        }

        /* Hero-блок: аватар + заголовки */
        .hero-section {
            display: flex;
            align-items: center;
            gap: 2rem;
            flex-wrap: wrap;
            margin-bottom: 0.5rem;
        }

        .avatar-huge {
            width: 170px;
            height: 170px;
            flex-shrink: 0;
            border-radius: 2px;
            object-fit: cover;
            border: 1px solid #6B6B6B;
            box-shadow: none;
            transition: all 0.2s ease;
            background: #2A2A2A;
        }

        .avatar-huge:hover {
            transform: scale(1.01);
            border-color: #8A8A8A;
            box-shadow: none;
        }

        .title-group {
            flex: 1;
        }

        .main-title {
            font-size: 3.4rem;
            font-weight: 800;
            letter-spacing: -1px;
            background: linear-gradient(135deg, #F0F0F0 0%, #A0A0A0 100%);
            background-clip: text;
            -webkit-background-clip: text;
            color: transparent;
            text-shadow: none;
        }

        .playlist-sub {
            font-size: 1rem;
            color: #B0B0B0;
            margin-bottom: 0.3rem;
            font-weight: 450;
            letter-spacing: -0.2px;
        }

        .track-stats {
            font-size: 0.85rem;
            font-family: monospace;
            color: #8A8A8A;
            border-bottom: 1px solid #2A2A2A;
            padding-bottom: 1rem;
            margin-bottom: 1.25rem;
            display: flex;
            gap: 0.75rem;
            margin-top: 0.25rem;
        }

        .track-stats span {
            background: #222222;
            padding: 0.2rem 0.8rem;
            border-radius: 30px;
            font-size: 0.75rem;
            font-family: monospace;
            letter-spacing: 0.3px;
            color: #B0B0B0;
        }

        /* СПИСОК ТРЕКОВ */
        .tracklist-container {
            max-height: 460px;
            overflow-y: auto;
            padding-right: 6px;
            margin-right: -6px;
        }

        .tracklist {
            display: flex;
            flex-direction: column;
            gap: 0px;
        }

        .track-row {
            display: flex;
            align-items: center;
            justify-content: space-between;
            padding: 0.9rem 0.5rem 0.9rem 0;
            border-bottom: 1px solid #262626;
            transition: background 0.2s ease;
            cursor: pointer;
        }

        .track-left {
            display: flex;
            align-items: baseline;
            gap: 1rem;
            flex-wrap: wrap;
            flex: 1;
        }

        .track-number {
            font-weight: 600;
            font-family: monospace;
            font-size: 1rem;
            color: #7A7A7A;
            width: 32px;
            transition: color 0.2s;
        }

        .track-info {
            display: flex;
            flex-direction: column;
        }

        .track-name {
            font-weight: 600;
            font-size: 1rem;
            color: #E0E0E0;
            letter-spacing: -0.2px;
        }

        .track-artist {
            font-size: 0.7rem;
            color: #9A9A9A;
            margin-top: 2px;
        }

        .track-duration {
            font-family: monospace;
            font-size: 0.85rem;
            color: #8A8A8A;
            background: transparent;
            padding: 0.2rem 0;
            transition: all 0.2s;
            display: flex;
            align-items: center;
            gap: 8px;
        }

        /* КНОПКА ПЛЕЯ (серая) */
        .play-btn {
            background: rgba(128, 128, 128, 0.18);
            border: none;
            border-radius: 30px;
            padding: 0.25rem 0.9rem;
            font-size: 0.7rem;
            font-weight: 500;
            font-family: monospace;
            color: #C0C0C0;
            cursor: pointer;
            transition: all 0.2s ease;
            backdrop-filter: blur(4px);
            letter-spacing: 0.3px;
        }

        .play-btn:hover {
            background: #4A4A4A;
            color: white;
            transform: scale(1.02);
        }

        /* Анимация при клике (серая) */
        .playing-effect {
            animation: pulseGray 0.5s ease-out;
            border-radius: 20px;
        }

        @keyframes pulseGray {
            0% { background: rgba(128, 128, 128, 0); }
            50% { background: rgba(128, 128, 128, 0.25); }
            100% { background: rgba(128, 128, 128, 0); }
        }

        .track-row:hover {
            background: #1A1A1A;
            border-radius: 18px;
            padding-left: 12px;
            padding-right: 12px;
            margin-left: -12px;
            margin-right: -6px;
            border-bottom-color: #3A3A3A;
        }

        .track-row:hover .track-number {
            color: #B0B0B0;
            text-shadow: 0 0 4px rgba(160, 160, 160, 0.4);
        }

        .track-row:hover .track-name {
            color: white;
        }

        .track-row:hover .track-artist {
            color: #C0C0C0;
        }

        .track-row:hover .track-duration {
            color: #D0D0D0;
        }

        /* МОДАЛЬНОЕ ОКНО С YOUTUBE (серые оттенки) */
        .youtube-modal {
            position: fixed;
            top: 0;
            left: 0;
            width: 100%;
            height: 100%;
            background-color: rgba(0, 0, 0, 0.85);
            backdrop-filter: blur(12px);
            z-index: 2000;
            display: flex;
            align-items: center;
            justify-content: center;
            opacity: 0;
            visibility: hidden;
            transition: opacity 0.3s ease, visibility 0s linear 0.3s;
        }

        .youtube-modal.active {
            opacity: 1;
            visibility: visible;
            transition: opacity 0.3s ease, visibility 0s linear 0s;
        }

        .modal-container {
            background: #111111;
            border-radius: 32px;
            width: 90%;
            max-width: 800px;
            overflow: hidden;
            box-shadow: 0 20px 30px -8px rgba(0,0,0,0.6), 0 0 0 1px rgba(101, 101, 101, 0.3);
            transform: scale(0.96);
            transition: transform 0.25s cubic-bezier(0.2, 0.95, 0.4, 1.05);
        }

        .active .modal-container {
            transform: scale(1);
        }

        .modal-header {
            display: flex;
            justify-content: space-between;
            align-items: center;
            padding: 1rem 1.5rem;
            background: #0A0A0A;
            border-bottom: 1px solid #222222;
        }

        .modal-title {
            font-weight: 600;
            color: #E0E0E0;
            font-size: 1rem;
        }

        .close-modal {
            font-size: 28px;
            font-weight: 300;
            color: #A0A0A0;
            cursor: pointer;
            transition: color 0.2s;
            line-height: 1;
        }

        .close-modal:hover {
            color: white;
        }

        .modal-body {
            padding: 1.5rem;
            background: #111111;
        }

        .youtube-iframe-wrapper {
            position: relative;
            width: 100%;
            padding-bottom: 56.25%;
            height: 0;
            border-radius: 20px;
            overflow: hidden;
        }

        .youtube-iframe-wrapper iframe {
            position: absolute;
            top: 0;
            left: 0;
            width: 100%;
            height: 100%;
            border: 0;
            border-radius: 16px;
        }

        .track-info-modal {
            margin-top: 1rem;
            text-align: center;
            color: #B0B0B0;
            font-size: 0.85rem;
        }

        /* скролл (серый) */
        .tracklist-container::-webkit-scrollbar {
            width: 4px;
        }
        .tracklist-container::-webkit-scrollbar-track {
            background: #1A1A1A;
            border-radius: 10px;
        }
        .tracklist-container::-webkit-scrollbar-thumb {
            background: #4A4A4A;
            border-radius: 10px;
        }

        @media (max-width: 720px) {
            .playlist-inner {
                padding: 1.3rem;
            }
            .hero-gradient-bg {
                padding: 1.3rem 1.3rem 0.5rem 1.3rem;
                margin: -1.3rem -1.3rem 0 -1.3rem;
            }
            .avatar-huge {
                width: 110px;
                height: 110px;
            }
            .main-title {
                font-size: 2.2rem;
            }
            .track-row {
                padding: 0.7rem 0;
            }
            .track-left {
                gap: 0.7rem;
            }
            .track-number {
                width: 28px;
                font-size: 0.9rem;
            }
            .forum-post {
                margin-top: 2rem;
            }
        }
    </style>
</head>
<body>
<div class="forum-post">
    <div class="playlist-card">
        <div class="playlist-inner">
            <!-- ГРАДИЕНТ ТОЛЬКО В ЗОНЕ ЗАГОЛОВКА И АВАТАРА -->
            <div class="hero-gradient-bg">
                <!-- БЛОК АВАТАР (170х170) + ЗАГОЛОВКИ -->
                <div class="hero-section">
                    <img id="characterAvatar" class="avatar-huge" src="https://upforme.ru/uploads/0017/24/ab/2/653386.jpg" alt="avatar">
                    <div class="title-group">
                        <div class="main-title">fire worshipper</div>
                        <div class="playlist-sub">плейлист<br>музыка для ночных поездок и спокойных мыслей</div>
                    </div>
                </div>
            </div>

            <!-- СТАТИСТИКА -->
            <div class="track-stats">
                <span>6 треков</span> <span>личная подборка</span>
            </div>

            <!-- СПИСОК ТРЕКОВ (YOUTUBE ID) -->
            <div class="tracklist-container">
                <div class="tracklist">
                    <!-- 1. Пустота - МУККА -->
                    <div class="track-row" data-track-name="Пустота" data-artist="МУККА" data-youtube-id="ffbzyjfu4pA">
                        <div class="track-left">
                            <span class="track-number">1</span>
                            <div class="track-info">
                                <div class="track-name">Пустота</div>
                                <div class="track-artist">МУККА</div>
                            </div>
                        </div>
                        <div class="track-duration">
                            <button class="play-btn">▶ слушать</button>
                        </div>
                    </div>
                    <!-- 2. Гори все огнем - этажность -->
                    <div class="track-row" data-track-name="Гори все огнем" data-artist="этажность" data-youtube-id="chxQXqyocR4">
                        <div class="track-left">
                            <span class="track-number">2</span>
                            <div class="track-info">
                                <div class="track-name">Гори все огнем</div>
                                <div class="track-artist">этажность</div>
                            </div>
                        </div>
                        <div class="track-duration">
                            <button class="play-btn">▶ слушать</button>
                        </div>
                    </div>
                    <!-- 3. Мы прячем лица в дыме - FIZICA, SOBOLIHA -->
                    <div class="track-row" data-track-name="Мы прячем лица в дыме" data-artist="FIZICA, SOBOLIHA" data-youtube-id="p6xa5AEfOSg">
                        <div class="track-left">
                            <span class="track-number">3</span>
                            <div class="track-info">
                                <div class="track-name">Мы прячем лица в дыме</div>
                                <div class="track-artist">FIZICA, SOBOLIHA</div>
                            </div>
                        </div>
                        <div class="track-duration">
                            <button class="play-btn">▶ слушать</button>
                        </div>
                    </div>
                    <!-- 4. Я поджигаю свой дом - ПИКЧИ! -->
                    <div class="track-row" data-track-name="Я поджигаю свой дом" data-artist="ПИКЧИ!" data-youtube-id="Lj5X3p-LBHA">
                        <div class="track-left">
                            <span class="track-number">4</span>
                            <div class="track-info">
                                <div class="track-name">Я поджигаю свой дом</div>
                                <div class="track-artist">ПИКЧИ!</div>
                            </div>
                        </div>
                        <div class="track-duration">
                            <button class="play-btn">▶ слушать</button>
                        </div>
                    </div>
                    <!-- 5. Спички - АнимациЯ -->
                    <div class="track-row" data-track-name="Спички" data-artist="АнимациЯ" data-youtube-id="MIKcZU9FnCo">
                        <div class="track-left">
                            <span class="track-number">5</span>
                            <div class="track-info">
                                <div class="track-name">Спички</div>
                                <div class="track-artist">АнимациЯ</div>
                            </div>
                        </div>
                        <div class="track-duration">
                            <button class="play-btn">▶ слушать</button>
                        </div>
                    </div>
                    <!-- 6. Погас и остыл - этажность -->
                    <div class="track-row" data-track-name="Погас и остыл" data-artist="этажность" data-youtube-id="xLUl4-sO_K4">
                        <div class="track-left">
                            <span class="track-number">6</span>
                            <div class="track-info">
                                <div class="track-name">Погас и остыл</div>
                                <div class="track-artist">этажность</div>
                            </div>
                        </div>
                        <div class="track-duration">
                            <button class="play-btn">▶ слушать</button>
                        </div>
                    </div>
                </div>
            </div>

            <!-- небольшая подпись -->
            <div style="text-align: center; font-size: 0.7rem; color: #6A6A6A; margin-top: 16px; font-family: monospace;">
                🎵 нажми «слушать» — откроется YouTube плеер
            </div>
        </div>
    </div>
</div>

<!-- МОДАЛЬНОЕ ОКНО С YOUTUBE -->
<div id="youtubeModal" class="youtube-modal">
    <div class="modal-container">
        <div class="modal-header">
            <div class="modal-title" id="modalTrackTitle">Сейчас играет</div>
            <div class="close-modal" id="closeModalBtn">&times;</div>
        </div>
        <div class="modal-body">
            <div class="youtube-iframe-wrapper">
                <iframe id="youtubeIframe" src="" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture" allowfullscreen></iframe>
            </div>
            <div class="track-info-modal" id="modalTrackInfo"></div>
        </div>
    </div>
</div>

<script>
    (function() {
        // Функция получения embed URL для YouTube
        function getYoutubeEmbedUrl(youtubeIdOrUrl) {
            if (!youtubeIdOrUrl) return null;
            let videoId = youtubeIdOrUrl;
            const patterns = [
                /(?:youtube\.com\/watch\?v=|youtu\.be\/)([^&?\/]+)/,
                /youtube\.com\/embed\/([^?\/]+)/
            ];
            for (let pattern of patterns) {
                const match = youtubeIdOrUrl.match(pattern);
                if (match && match[1]) {
                    videoId = match[1];
                    break;
                }
            }
            if (videoId && videoId.length >= 8 && !videoId.includes('http')) {
                return `https://www.youtube.com/embed/${videoId}?autoplay=1&enablejsapi=1&origin=${encodeURIComponent(window.location.origin)}`;
            }
            return null;
        }
       
        const modal = document.getElementById('youtubeModal');
        const closeModalBtn = document.getElementById('closeModalBtn');
        const youtubeIframe = document.getElementById('youtubeIframe');
        const modalTrackTitle = document.getElementById('modalTrackTitle');
        const modalTrackInfo = document.getElementById('modalTrackInfo');
       
        function openYoutubeModal(videoIdOrUrl, trackName, artistName) {
            const embedUrl = getYoutubeEmbedUrl(videoIdOrUrl);
            if (!embedUrl) {
                alert("Не удалось распознать ссылку на YouTube. Проверьте data-youtube-id у трека.");
                return;
            }
            youtubeIframe.src = embedUrl;
            modalTrackTitle.innerText = `${trackName} — ${artistName}`;
            modalTrackInfo.innerText = `🎧 Слушайте "${trackName}" на YouTube`;
            modal.classList.add('active');
            document.body.style.overflow = 'hidden';
        }
       
        function closeModal() {
            modal.classList.remove('active');
            document.body.style.overflow = '';
            youtubeIframe.src = '';
        }
       
        if (closeModalBtn) {
            closeModalBtn.addEventListener('click', closeModal);
        }
       
        modal.addEventListener('click', (e) => {
            if (e.target === modal) {
                closeModal();
            }
        });
       
        const trackRows = document.querySelectorAll('.track-row');
        trackRows.forEach(row => {
            const playBtn = row.querySelector('.play-btn');
            if (!playBtn) return;
           
            const youtubeId = row.getAttribute('data-youtube-id');
            const trackName = row.getAttribute('data-track-name');
            const artistName = row.getAttribute('data-artist');
           
            playBtn.addEventListener('click', (e) => {
                e.stopPropagation();
                if (!youtubeId) {
                    alert("Для этого трека не указана ссылка YouTube. Добавьте data-youtube-id с ID видео.");
                    return;
                }
                row.classList.add('playing-effect');
                setTimeout(() => row.classList.remove('playing-effect'), 500);
                openYoutubeModal(youtubeId, trackName, artistName);
            });
           
            row.addEventListener('click', (e) => {
                if (e.target === playBtn || playBtn.contains(e.target)) return;
                if (!youtubeId) return;
                row.classList.add('playing-effect');
                setTimeout(() => row.classList.remove('playing-effect'), 500);
                openYoutubeModal(youtubeId, trackName, artistName);
            });
        });
       
        // плавное появление треков
        const allTracks = document.querySelectorAll('.track-row');
        allTracks.forEach((track, idx) => {
            track.style.opacity = '0';
            track.style.transform = 'translateY(6px)';
            track.style.transition = `opacity 0.2s cubic-bezier(0.2, 0.9, 0.4, 1) ${idx * 0.02}s, transform 0.25s ease ${idx * 0.02}s`;
            setTimeout(() => {
                track.style.opacity = '1';
                track.style.transform = 'translateY(0)';
            }, 50);
        });
       
        document.addEventListener('keydown', (e) => {
            if (e.key === 'Escape' && modal.classList.contains('active')) {
                closeModal();
            }
        });
    })();
</script>
</body>
</html>[/html]

0

216

[html]<!DOCTYPE html>
<html lang="ru">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=yes">
    <title>fire worshipper · плейлист персонажа</title>
    <style>
        /* ---------- ОСНОВНОЙ СТИЛЬ: серые оттенки, градиент только вверху, без теней и анимации движения ---------- */
        * {
            margin: 0;
            padding: 0;
            box-sizing: border-box;
        }

        body {
            background: #0B0C10;
            display: flex;
            align-items: center;
            justify-content: center;
            min-height: 100vh;
            font-family: 'Inter', -apple-system, 'Segoe UI', 'SF Pro Text', 'Poppins', system-ui, sans-serif;
            padding: 2rem;
        }

        .forum-post {
            max-width: 980px;
            width: 100%;
            margin: 0 auto;
            margin-top: 3rem;
        }

        .playlist-card {
            background: #0F1111;  /* сплошной тёмный фон для всей карточки */
            border-radius: 32px;
            overflow: hidden;
            border: 1px solid rgba(255, 255, 255, 0.06);
            /* тень полностью убрана */
            box-shadow: none;
            /* анимация движения убрана */
            transition: none;
        }

        /* градиентная обёртка только для верхней части (аватар + заголовки) */
        .hero-gradient-bg {
            background: linear-gradient(180deg, #343434 0%, #0F1111 100%);
            border-radius: 32px 32px 0 0;
            padding: 2rem 2rem 1rem 2rem;
            margin: -2rem -2rem 0 -2rem;
        }

        /* компенсация внутренних отступов */
        .playlist-inner {
            padding: 2rem 2rem 2rem 2rem;
        }

        /* Hero-блок: аватар + заголовки */
        .hero-section {
            display: flex;
            align-items: center;
            gap: 2rem;
            flex-wrap: wrap;
            margin-bottom: 0.5rem;
        }

        .avatar-huge {
            width: 170px;
            height: 170px;
            flex-shrink: 0;
            border-radius: 2px;
            object-fit: cover;
            border: 1px solid #6B6B6B;
            box-shadow: none;
            transition: all 0.2s ease;
            background: #2A2A2A;
        }

        .avatar-huge:hover {
            transform: scale(1.01);
            border-color: #8A8A8A;
            box-shadow: none;
        }

        .title-group {
            flex: 1;
        }

        .main-title {
            font-size: 3.4rem;
            font-weight: 800;
            letter-spacing: -1px;
            background: linear-gradient(135deg, #F0F0F0 0%, #A0A0A0 100%);
            background-clip: text;
            -webkit-background-clip: text;
            color: transparent;
            text-shadow: none;
        }

        .playlist-sub {
            font-size: 1rem;
            color: #B0B0B0;
            margin-bottom: 0.3rem;
            font-weight: 450;
            letter-spacing: -0.2px;
        }

        .track-stats {
            font-size: 0.85rem;
            font-family: monospace;
            color: #8A8A8A;
            border-bottom: 1px solid #2A2A2A;
            padding-bottom: 1rem;
            margin-bottom: 1.25rem;
            display: flex;
            gap: 0.75rem;
            margin-top: 0.25rem;
        }

        .track-stats span {
            background: #222222;
            padding: 0.2rem 0.8rem;
            border-radius: 30px;
            font-size: 0.75rem;
            font-family: monospace;
            letter-spacing: 0.3px;
            color: #B0B0B0;
        }

        /* СПИСОК ТРЕКОВ */
        .tracklist-container {
            max-height: 460px;
            overflow-y: auto;
            padding-right: 6px;
            margin-right: -6px;
        }

        .tracklist {
            display: flex;
            flex-direction: column;
            gap: 0px;
        }

        .track-row {
            display: flex;
            align-items: center;
            justify-content: space-between;
            padding: 0.9rem 0.5rem 0.9rem 0;
            border-bottom: 1px solid #262626;
            transition: background 0.2s ease;
            cursor: pointer;
        }

        .track-left {
            display: flex;
            align-items: baseline;
            gap: 1rem;
            flex-wrap: wrap;
            flex: 1;
        }

        .track-number {
            font-weight: 600;
            font-family: monospace;
            font-size: 1rem;
            color: #7A7A7A;
            width: 32px;
            transition: color 0.2s;
        }

        .track-info {
            display: flex;
            flex-direction: column;
        }

        .track-name {
            font-weight: 600;
            font-size: 1rem;
            color: #E0E0E0;
            letter-spacing: -0.2px;
        }

        .track-artist {
            font-size: 0.7rem;
            color: #9A9A9A;
            margin-top: 2px;
        }

        .track-duration {
            font-family: monospace;
            font-size: 0.85rem;
            color: #8A8A8A;
            background: transparent;
            padding: 0.2rem 0;
            transition: all 0.2s;
            display: flex;
            align-items: center;
            gap: 8px;
        }

        /* КНОПКА ПЛЕЯ (серая) */
        .play-btn {
            background: rgba(128, 128, 128, 0.18);
            border: none;
            border-radius: 30px;
            padding: 0.25rem 0.9rem;
            font-size: 0.7rem;
            font-weight: 500;
            font-family: monospace;
            color: #C0C0C0;
            cursor: pointer;
            transition: all 0.2s ease;
            backdrop-filter: blur(4px);
            letter-spacing: 0.3px;
        }

        .play-btn:hover {
            background: #4A4A4A;
            color: white;
            transform: scale(1.02);
        }

        /* Анимация при клике (серая) */
        .playing-effect {
            animation: pulseGray 0.5s ease-out;
            border-radius: 20px;
        }

        @keyframes pulseGray {
            0% { background: rgba(128, 128, 128, 0); }
            50% { background: rgba(128, 128, 128, 0.25); }
            100% { background: rgba(128, 128, 128, 0); }
        }

        .track-row:hover {
            background: #1A1A1A;
            border-radius: 18px;
            padding-left: 12px;
            padding-right: 12px;
            margin-left: -12px;
            margin-right: -6px;
            border-bottom-color: #3A3A3A;
        }

        .track-row:hover .track-number {
            color: #B0B0B0;
            text-shadow: 0 0 4px rgba(160, 160, 160, 0.4);
        }

        .track-row:hover .track-name {
            color: white;
        }

        .track-row:hover .track-artist {
            color: #C0C0C0;
        }

        .track-row:hover .track-duration {
            color: #D0D0D0;
        }

        /* МОДАЛЬНОЕ ОКНО С YOUTUBE (серые оттенки) */
        .youtube-modal {
            position: fixed;
            top: 0;
            left: 0;
            width: 100%;
            height: 100%;
            background-color: rgba(0, 0, 0, 0.85);
            backdrop-filter: blur(12px);
            z-index: 2000;
            display: flex;
            align-items: center;
            justify-content: center;
            opacity: 0;
            visibility: hidden;
            transition: opacity 0.3s ease, visibility 0s linear 0.3s;
        }

        .youtube-modal.active {
            opacity: 1;
            visibility: visible;
            transition: opacity 0.3s ease, visibility 0s linear 0s;
        }

        .modal-container {
            background: #111111;
            border-radius: 32px;
            width: 90%;
            max-width: 800px;
            overflow: hidden;
            box-shadow: 0 20px 30px -8px rgba(0,0,0,0.6), 0 0 0 1px rgba(101, 101, 101, 0.3);
            transform: scale(0.96);
            transition: transform 0.25s cubic-bezier(0.2, 0.95, 0.4, 1.05);
        }

        .active .modal-container {
            transform: scale(1);
        }

        .modal-header {
            display: flex;
            justify-content: space-between;
            align-items: center;
            padding: 1rem 1.5rem;
            background: #0A0A0A;
            border-bottom: 1px solid #222222;
        }

        .modal-title {
            font-weight: 600;
            color: #E0E0E0;
            font-size: 1rem;
        }

        .close-modal {
            font-size: 28px;
            font-weight: 300;
            color: #A0A0A0;
            cursor: pointer;
            transition: color 0.2s;
            line-height: 1;
        }

        .close-modal:hover {
            color: white;
        }

        .modal-body {
            padding: 1.5rem;
            background: #111111;
        }

        .youtube-iframe-wrapper {
            position: relative;
            width: 100%;
            padding-bottom: 56.25%;
            height: 0;
            border-radius: 20px;
            overflow: hidden;
        }

        .youtube-iframe-wrapper iframe {
            position: absolute;
            top: 0;
            left: 0;
            width: 100%;
            height: 100%;
            border: 0;
            border-radius: 16px;
        }

        .track-info-modal {
            margin-top: 1rem;
            text-align: center;
            color: #B0B0B0;
            font-size: 0.85rem;
        }

        /* скролл (серый) */
        .tracklist-container::-webkit-scrollbar {
            width: 4px;
        }
        .tracklist-container::-webkit-scrollbar-track {
            background: #1A1A1A;
            border-radius: 10px;
        }
        .tracklist-container::-webkit-scrollbar-thumb {
            background: #4A4A4A;
            border-radius: 10px;
        }

        @media (max-width: 720px) {
            .playlist-inner {
                padding: 1.3rem;
            }
            .hero-gradient-bg {
                padding: 1.3rem 1.3rem 0.5rem 1.3rem;
                margin: -1.3rem -1.3rem 0 -1.3rem;
            }
            .avatar-huge {
                width: 110px;
                height: 110px;
            }
            .main-title {
                font-size: 2.2rem;
            }
            .track-row {
                padding: 0.7rem 0;
            }
            .track-left {
                gap: 0.7rem;
            }
            .track-number {
                width: 28px;
                font-size: 0.9rem;
            }
            .forum-post {
                margin-top: 2rem;
            }
        }
    </style>
</head>
<body>
<div class="forum-post">
    <div class="playlist-card">
        <div class="playlist-inner">
            <!-- ГРАДИЕНТ ТОЛЬКО В ЗОНЕ ЗАГОЛОВКА И АВАТАРА -->
            <div class="hero-gradient-bg">
                <!-- БЛОК АВАТАР (170х170) + ЗАГОЛОВКИ -->
                <div class="hero-section">
                    <img id="characterAvatar" class="avatar-huge" src="https://upforme.ru/uploads/0017/24/ab/2/680403.jpg" alt="avatar">
                    <div class="title-group">
                        <div class="main-title">элио</div>
                        <div class="playlist-sub">плейлист<br>не причиняйте вреда рабу судьбы и не позволяйте ему утратить способность к независимому мышлению</div>
                    </div>
                </div>
            </div>

            <!-- СТАТИСТИКА -->
            <div class="track-stats">
                <span>4 трека</span> <span>личная подборка</span>
            </div>

            <!-- СПИСОК ТРЕКОВ (YOUTUBE ID) -->
            <div class="tracklist-container">
                <div class="tracklist">
                    <!-- 1. Пустота - МУККА -->
                    <div class="track-row" data-track-name="Бэкап" data-artist="DEEP-EX-SENSE, ЛЖЕДМИТРИЙ IV" data-youtube-id="vGQahCb42HU">
                        <div class="track-left">
                            <span class="track-number">1</span>
                            <div class="track-info">
                                <div class="track-name">Бэкап</div>
                                <div class="track-artist">DEEP-EX-SENSE, ЛЖЕДМИТРИЙ IV</div>
                            </div>
                        </div>
                        <div class="track-duration">
                            <button class="play-btn">▶ слушать</button>
                        </div>
                    </div>
                    <!-- 2. Гори все огнем - этажность -->
                    <div class="track-row" data-track-name="No Longer You" data-artist="Epic The Musical" data-youtube-id="BZ8qL5P270Q">
                        <div class="track-left">
                            <span class="track-number">2</span>
                            <div class="track-info">
                                <div class="track-name">No Longer You</div>
                                <div class="track-artist">Epic The Musical</div>
                            </div>
                        </div>
                        <div class="track-duration">
                            <button class="play-btn">▶ слушать</button>
                        </div>
                    </div>
                    <!-- 3. Мы прячем лица в дыме - FIZICA, SOBOLIHA -->
                    <div class="track-row" data-track-name="V.A.N" data-artist="BAD OMENS x POPPY" data-youtube-id="ev_vPbqUlZs">
                        <div class="track-left">
                            <span class="track-number">3</span>
                            <div class="track-info">
                                <div class="track-name">V.A.N</div>
                                <div class="track-artist">BAD OMENS x POPPY</div>
                            </div>
                        </div>
                        <div class="track-duration">
                            <button class="play-btn">▶ слушать</button>
                        </div>
                    </div>
                    <!-- 4. Я поджигаю свой дом - ПИКЧИ! -->
                    <div class="track-row" data-track-name="Битум" data-artist="OMNIXX" data-youtube-id="VbRYPcpQlFk">
                        <div class="track-left">
                            <span class="track-number">4</span>
                            <div class="track-info">
                                <div class="track-name">Битум</div>
                                <div class="track-artist">OMNIXX</div>
                            </div>
                        </div>
                        <div class="track-duration">
                            <button class="play-btn">▶ слушать</button>
                        </div>
                    </div>

                        <div class="track-duration">
                            <button class="play-btn">▶ слушать</button>
                        </div>
                    </div>
                </div>
            </div>

            <!-- небольшая подпись -->
            <div style="text-align: center; font-size: 0.7rem; color: #6A6A6A; margin-top: 16px; font-family: monospace;">
                🎵 нажми «слушать» — откроется YouTube плеер
            </div>
        </div>
    </div>
</div>

<!-- МОДАЛЬНОЕ ОКНО С YOUTUBE -->
<div id="youtubeModal" class="youtube-modal">
    <div class="modal-container">
        <div class="modal-header">
            <div class="modal-title" id="modalTrackTitle">Сейчас играет</div>
            <div class="close-modal" id="closeModalBtn">&times;</div>
        </div>
        <div class="modal-body">
            <div class="youtube-iframe-wrapper">
                <iframe id="youtubeIframe" src="" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture" allowfullscreen></iframe>
            </div>
            <div class="track-info-modal" id="modalTrackInfo"></div>
        </div>
    </div>
</div>

<script>
    (function() {
        // Функция получения embed URL для YouTube
        function getYoutubeEmbedUrl(youtubeIdOrUrl) {
            if (!youtubeIdOrUrl) return null;
            let videoId = youtubeIdOrUrl;
            const patterns = [
                /(?:youtube\.com\/watch\?v=|youtu\.be\/)([^&?\/]+)/,
                /youtube\.com\/embed\/([^?\/]+)/
            ];
            for (let pattern of patterns) {
                const match = youtubeIdOrUrl.match(pattern);
                if (match && match[1]) {
                    videoId = match[1];
                    break;
                }
            }
            if (videoId && videoId.length >= 8 && !videoId.includes('http')) {
                return `https://www.youtube.com/embed/${videoId}?autoplay=1&enablejsapi=1&origin=${encodeURIComponent(window.location.origin)}`;
            }
            return null;
        }
       
        const modal = document.getElementById('youtubeModal');
        const closeModalBtn = document.getElementById('closeModalBtn');
        const youtubeIframe = document.getElementById('youtubeIframe');
        const modalTrackTitle = document.getElementById('modalTrackTitle');
        const modalTrackInfo = document.getElementById('modalTrackInfo');
       
        function openYoutubeModal(videoIdOrUrl, trackName, artistName) {
            const embedUrl = getYoutubeEmbedUrl(videoIdOrUrl);
            if (!embedUrl) {
                alert("Не удалось распознать ссылку на YouTube. Проверьте data-youtube-id у трека.");
                return;
            }
            youtubeIframe.src = embedUrl;
            modalTrackTitle.innerText = `${trackName} — ${artistName}`;
            modalTrackInfo.innerText = `🎧 Слушайте "${trackName}" на YouTube`;
            modal.classList.add('active');
            document.body.style.overflow = 'hidden';
        }
       
        function closeModal() {
            modal.classList.remove('active');
            document.body.style.overflow = '';
            youtubeIframe.src = '';
        }
       
        if (closeModalBtn) {
            closeModalBtn.addEventListener('click', closeModal);
        }
       
        modal.addEventListener('click', (e) => {
            if (e.target === modal) {
                closeModal();
            }
        });
       
        const trackRows = document.querySelectorAll('.track-row');
        trackRows.forEach(row => {
            const playBtn = row.querySelector('.play-btn');
            if (!playBtn) return;
           
            const youtubeId = row.getAttribute('data-youtube-id');
            const trackName = row.getAttribute('data-track-name');
            const artistName = row.getAttribute('data-artist');
           
            playBtn.addEventListener('click', (e) => {
                e.stopPropagation();
                if (!youtubeId) {
                    alert("Для этого трека не указана ссылка YouTube. Добавьте data-youtube-id с ID видео.");
                    return;
                }
                row.classList.add('playing-effect');
                setTimeout(() => row.classList.remove('playing-effect'), 500);
                openYoutubeModal(youtubeId, trackName, artistName);
            });
           
            row.addEventListener('click', (e) => {
                if (e.target === playBtn || playBtn.contains(e.target)) return;
                if (!youtubeId) return;
                row.classList.add('playing-effect');
                setTimeout(() => row.classList.remove('playing-effect'), 500);
                openYoutubeModal(youtubeId, trackName, artistName);
            });
        });
       
        // плавное появление треков
        const allTracks = document.querySelectorAll('.track-row');
        allTracks.forEach((track, idx) => {
            track.style.opacity = '0';
            track.style.transform = 'translateY(6px)';
            track.style.transition = `opacity 0.2s cubic-bezier(0.2, 0.9, 0.4, 1) ${idx * 0.02}s, transform 0.25s ease ${idx * 0.02}s`;
            setTimeout(() => {
                track.style.opacity = '1';
                track.style.transform = 'translateY(0)';
            }, 50);
        });
       
        document.addEventListener('keydown', (e) => {
            if (e.key === 'Escape' && modal.classList.contains('active')) {
                closeModal();
            }
        });
    })();
</script>
</body>
</html>[/html]

0

217

[html]<!DOCTYPE html>
<html lang="ru">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=yes">
    <title>элио · плейлист персонажа</title>
    <style>
        /* ---------- ОСНОВНОЙ СТИЛЬ: серые оттенки, градиент только вверху, без теней и анимации движения ---------- */
        * {
            margin: 0;
            padding: 0;
            box-sizing: border-box;
        }

        body {
            background: #0B0C10;
            display: flex;
            align-items: center;
            justify-content: center;
            min-height: 100vh;
            font-family: 'Inter', -apple-system, 'Segoe UI', 'SF Pro Text', 'Poppins', system-ui, sans-serif;
            padding: 2rem;
        }

        .forum-post {
            max-width: 980px;
            width: 100%;
            margin: 0 auto;
            margin-top: 3rem;
        }

        .playlist-card {
            background: #0F1111;
            border-radius: 32px;
            overflow: hidden;
            border: 1px solid rgba(255, 255, 255, 0.06);
            box-shadow: none;
            transition: none;
        }

        .hero-gradient-bg {
            background: linear-gradient(180deg, #343434 0%, #0F1111 100%);
            border-radius: 32px 32px 0 0;
            padding: 2rem 2rem 1rem 2rem;
            margin: -2rem -2rem 0 -2rem;
        }

        .playlist-inner {
            padding: 2rem 2rem 2rem 2rem;
        }

        .hero-section {
            display: flex;
            align-items: center;
            gap: 2rem;
            flex-wrap: wrap;
            margin-bottom: 0.5rem;
        }

        .avatar-huge {
            width: 170px;
            height: 170px;
            flex-shrink: 0;
            border-radius: 2px;
            object-fit: cover;
            border: 1px solid #6B6B6B;
            box-shadow: none;
            transition: all 0.2s ease;
            background: #2A2A2A;
        }

        .avatar-huge:hover {
            transform: scale(1.01);
            border-color: #8A8A8A;
            box-shadow: none;
        }

        .title-group {
            flex: 1;
        }

        .main-title {
            font-size: 3.4rem;
            font-weight: 800;
            letter-spacing: -1px;
            background: linear-gradient(135deg, #F0F0F0 0%, #A0A0A0 100%);
            background-clip: text;
            -webkit-background-clip: text;
            color: transparent;
            text-shadow: none;
        }

        .playlist-sub {
            font-size: 1rem;
            color: #B0B0B0;
            margin-bottom: 0.3rem;
            font-weight: 450;
            letter-spacing: -0.2px;
        }

        .track-stats {
            font-size: 0.85rem;
            font-family: monospace;
            color: #8A8A8A;
            border-bottom: 1px solid #2A2A2A;
            padding-bottom: 1rem;
            margin-bottom: 1.25rem;
            display: flex;
            gap: 0.75rem;
            margin-top: 0.25rem;
        }

        .track-stats span {
            background: #222222;
            padding: 0.2rem 0.8rem;
            border-radius: 30px;
            font-size: 0.75rem;
            font-family: monospace;
            letter-spacing: 0.3px;
            color: #B0B0B0;
        }

        .tracklist-container {
            max-height: 460px;
            overflow-y: auto;
            padding-right: 6px;
            margin-right: -6px;
        }

        .tracklist {
            display: flex;
            flex-direction: column;
            gap: 0px;
        }

        .track-row {
            display: flex;
            align-items: center;
            justify-content: space-between;
            padding: 0.9rem 0.5rem 0.9rem 0;
            border-bottom: 1px solid #262626;
            transition: background 0.2s ease;
            cursor: pointer;
        }

        .track-left {
            display: flex;
            align-items: baseline;
            gap: 1rem;
            flex-wrap: wrap;
            flex: 1;
        }

        .track-number {
            font-weight: 600;
            font-family: monospace;
            font-size: 1rem;
            color: #7A7A7A;
            width: 32px;
            transition: color 0.2s;
        }

        .track-info {
            display: flex;
            flex-direction: column;
        }

        .track-name {
            font-weight: 600;
            font-size: 1rem;
            color: #E0E0E0;
            letter-spacing: -0.2px;
        }

        .track-artist {
            font-size: 0.7rem;
            color: #9A9A9A;
            margin-top: 2px;
        }

        .track-duration {
            font-family: monospace;
            font-size: 0.85rem;
            color: #8A8A8A;
            background: transparent;
            padding: 0.2rem 0;
            transition: all 0.2s;
            display: flex;
            align-items: center;
            gap: 8px;
        }

        .play-btn {
            background: rgba(128, 128, 128, 0.18);
            border: none;
            border-radius: 30px;
            padding: 0.25rem 0.9rem;
            font-size: 0.7rem;
            font-weight: 500;
            font-family: monospace;
            color: #C0C0C0;
            cursor: pointer;
            transition: all 0.2s ease;
            backdrop-filter: blur(4px);
            letter-spacing: 0.3px;
        }

        .play-btn:hover {
            background: #4A4A4A;
            color: white;
            transform: scale(1.02);
        }

        .playing-effect {
            animation: pulseGray 0.5s ease-out;
            border-radius: 20px;
        }

        @keyframes pulseGray {
            0% { background: rgba(128, 128, 128, 0); }
            50% { background: rgba(128, 128, 128, 0.25); }
            100% { background: rgba(128, 128, 128, 0); }
        }

        .track-row:hover {
            background: #1A1A1A;
            border-radius: 18px;
            padding-left: 12px;
            padding-right: 12px;
            margin-left: -12px;
            margin-right: -6px;
            border-bottom-color: #3A3A3A;
        }

        .track-row:hover .track-number {
            color: #B0B0B0;
            text-shadow: 0 0 4px rgba(160, 160, 160, 0.4);
        }

        .track-row:hover .track-name {
            color: white;
        }

        .track-row:hover .track-artist {
            color: #C0C0C0;
        }

        .track-row:hover .track-duration {
            color: #D0D0D0;
        }

        .youtube-modal {
            position: fixed;
            top: 0;
            left: 0;
            width: 100%;
            height: 100%;
            background-color: rgba(0, 0, 0, 0.85);
            backdrop-filter: blur(12px);
            z-index: 2000;
            display: flex;
            align-items: center;
            justify-content: center;
            opacity: 0;
            visibility: hidden;
            transition: opacity 0.3s ease, visibility 0s linear 0.3s;
        }

        .youtube-modal.active {
            opacity: 1;
            visibility: visible;
            transition: opacity 0.3s ease, visibility 0s linear 0s;
        }

        .modal-container {
            background: #111111;
            border-radius: 32px;
            width: 90%;
            max-width: 800px;
            overflow: hidden;
            box-shadow: 0 20px 30px -8px rgba(0,0,0,0.6), 0 0 0 1px rgba(101, 101, 101, 0.3);
            transform: scale(0.96);
            transition: transform 0.25s cubic-bezier(0.2, 0.95, 0.4, 1.05);
        }

        .active .modal-container {
            transform: scale(1);
        }

        .modal-header {
            display: flex;
            justify-content: space-between;
            align-items: center;
            padding: 1rem 1.5rem;
            background: #0A0A0A;
            border-bottom: 1px solid #222222;
        }

        .modal-title {
            font-weight: 600;
            color: #E0E0E0;
            font-size: 1rem;
        }

        .close-modal {
            font-size: 28px;
            font-weight: 300;
            color: #A0A0A0;
            cursor: pointer;
            transition: color 0.2s;
            line-height: 1;
        }

        .close-modal:hover {
            color: white;
        }

        .modal-body {
            padding: 1.5rem;
            background: #111111;
        }

        .youtube-iframe-wrapper {
            position: relative;
            width: 100%;
            padding-bottom: 56.25%;
            height: 0;
            border-radius: 20px;
            overflow: hidden;
        }

        .youtube-iframe-wrapper iframe {
            position: absolute;
            top: 0;
            left: 0;
            width: 100%;
            height: 100%;
            border: 0;
            border-radius: 16px;
        }

        .track-info-modal {
            margin-top: 1rem;
            text-align: center;
            color: #B0B0B0;
            font-size: 0.85rem;
        }

        .tracklist-container::-webkit-scrollbar {
            width: 4px;
        }
        .tracklist-container::-webkit-scrollbar-track {
            background: #1A1A1A;
            border-radius: 10px;
        }
        .tracklist-container::-webkit-scrollbar-thumb {
            background: #4A4A4A;
            border-radius: 10px;
        }

        @media (max-width: 720px) {
            .playlist-inner {
                padding: 1.3rem;
            }
            .hero-gradient-bg {
                padding: 1.3rem 1.3rem 0.5rem 1.3rem;
                margin: -1.3rem -1.3rem 0 -1.3rem;
            }
            .avatar-huge {
                width: 110px;
                height: 110px;
            }
            .main-title {
                font-size: 2.2rem;
            }
            .track-row {
                padding: 0.7rem 0;
            }
            .track-left {
                gap: 0.7rem;
            }
            .track-number {
                width: 28px;
                font-size: 0.9rem;
            }
            .forum-post {
                margin-top: 2rem;
            }
        }
    </style>
</head>
<body>
<div class="forum-post">
    <div class="playlist-card">
        <div class="playlist-inner">
            <div class="hero-gradient-bg">
                <div class="hero-section">
                    <img id="characterAvatar" class="avatar-huge" src="https://upforme.ru/uploads/0017/24/ab/2/99630.jpg" alt="avatar">
                    <div class="title-group">
                        <div class="main-title">ракшор</div>
                        <div class="playlist-sub">плейлист<br>разве не знаете, что мы будем судить ангелов, не тем ли более дела житейские</div>
                    </div>
                </div>
            </div>

            <div class="track-stats">
                <span>7 треков</span> <span>личная подборка</span>
            </div>

            <div class="tracklist-container">
                <div class="tracklist">
                    <!-- 1. Мне очень высоко - PyLai (ССЫЛКА НА ЯНДЕКС МУЗЫКУ) -->
                    <div class="track-row" data-track-name="Мне очень высоко" data-artist="PyLai" data-is-yandex="true" data-yandex-url="https://music.yandex.ru/album/11796992/track/70043665?utm_medium=copy_link&ref_id=a86a08d3-dda0-4887-bb9c-e18d3b12632a">
                        <div class="track-left">
                            <span class="track-number">1</span>
                            <div class="track-info">
                                <div class="track-name">Мне очень высоко</div>
                                <div class="track-artist">PyLai</div>
                            </div>
                        </div>
                        <div class="track-duration">
                            <button class="play-btn">▶ слушать</button>
                        </div>
                    </div>
                    <!-- 2. Крылья - лампабикт (YouTube) -->
                    <div class="track-row" data-track-name="Крылья" data-artist="лампабикт" data-youtube-id="yv8WU3rlUTQ">
                        <div class="track-left">
                            <span class="track-number">2</span>
                            <div class="track-info">
                                <div class="track-name">Крылья</div>
                                <div class="track-artist">лампабикт</div>
                            </div>
                        </div>
                        <div class="track-duration">
                            <button class="play-btn">▶ слушать</button>
                        </div>
                    </div>
                    <!-- 3. Куклы - Обе-Рек (YouTube) -->
                    <div class="track-row" data-track-name="Куклы" data-artist="Обе-Рек" data-youtube-id="F5dP2uZupCE">
                        <div class="track-left">
                            <span class="track-number">3</span>
                            <div class="track-info">
                                <div class="track-name">Куклы</div>
                                <div class="track-artist">Обе-Рек</div>
                            </div>
                        </div>
                        <div class="track-duration">
                            <button class="play-btn">▶ слушать</button>
                        </div>
                    </div>
                    <!-- 4. Молот ведьм - ТЕППО (YouTube) -->
                    <div class="track-row" data-track-name="Молот ведьм" data-artist="ТЕППО" data-youtube-id="fPHSaRDgqGo">
                        <div class="track-left">
                            <span class="track-number">4</span>
                            <div class="track-info">
                                <div class="track-name">Молот ведьм</div>
                                <div class="track-artist">ТЕППО</div>
                            </div>
                        </div>
                        <div class="track-duration">
                            <button class="play-btn">▶ слушать</button>
                        </div>
                    </div>
                    <!-- 5. Икар - The Meto (YouTube) -->
                    <div class="track-row" data-track-name="Икар" data-artist="The Meto" data-youtube-id="9bdim0Y0IRs">
                        <div class="track-left">
                            <span class="track-number">5</span>
                            <div class="track-info">
                                <div class="track-name">Икар</div>
                                <div class="track-artist">The Meto</div>
                            </div>
                        </div>
                        <div class="track-duration">
                            <button class="play-btn">▶ слушать</button>
                        </div>
                    </div>
                    <!-- 6. Порвется небо - Unit (YouTube) -->
                    <div class="track-row" data-track-name="Порвется небо" data-artist="Unit" data-youtube-id="1OWj5U0BshI">
                        <div class="track-left">
                            <span class="track-number">6</span>
                            <div class="track-info">
                                <div class="track-name">Порвется небо</div>
                                <div class="track-artist">Unit</div>
                            </div>
                        </div>
                        <div class="track-duration">
                            <button class="play-btn">▶ слушать</button>
                        </div>
                    </div>
                    <!-- 7. Телу тоже больно - найтивыход (YouTube) -->
                    <div class="track-row" data-track-name="Телу тоже больно" data-artist="найтивыход" data-youtube-id="jvJVUAJDO-M">
                        <div class="track-left">
                            <span class="track-number">7</span>
                            <div class="track-info">
                                <div class="track-name">Телу тоже больно</div>
                                <div class="track-artist">найтивыход</div>
                            </div>
                        </div>
                        <div class="track-duration">
                            <button class="play-btn">▶ слушать</button>
                        </div>
                    </div>
                </div>
            </div>

            <div style="text-align: center; font-size: 0.7rem; color: #6A6A6A; margin-top: 16px; font-family: monospace;">
                🎵 нажми «слушать» — откроется плеер (Яндекс Музыка или YouTube)
            </div>
        </div>
    </div>
</div>

<div id="youtubeModal" class="youtube-modal">
    <div class="modal-container">
        <div class="modal-header">
            <div class="modal-title" id="modalTrackTitle">Сейчас играет</div>
            <div class="close-modal" id="closeModalBtn">&times;</div>
        </div>
        <div class="modal-body">
            <div class="youtube-iframe-wrapper">
                <iframe id="youtubeIframe" src="" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture" allowfullscreen></iframe>
            </div>
            <div class="track-info-modal" id="modalTrackInfo"></div>
        </div>
    </div>
</div>

<script>
    (function() {
        function getYoutubeEmbedUrl(youtubeIdOrUrl) {
            if (!youtubeIdOrUrl) return null;
            let videoId = youtubeIdOrUrl;
            const patterns = [
                /(?:youtube\.com\/watch\?v=|youtu\.be\/)([^&?\/]+)/,
                /youtube\.com\/embed\/([^?\/]+)/
            ];
            for (let pattern of patterns) {
                const match = youtubeIdOrUrl.match(pattern);
                if (match && match[1]) {
                    videoId = match[1];
                    break;
                }
            }
            if (videoId && videoId.length >= 8 && !videoId.includes('http')) {
                return `https://www.youtube.com/embed/${videoId}?autoplay=1&enablejsapi=1&origin=${encodeURIComponent(window.location.origin)}`;
            }
            return null;
        }
       
        function openYandexMusic(url, trackName, artistName) {
            // Открываем ссылку на Яндекс Музыку в новой вкладке
            window.open(url, '_blank');
            // Показываем небольшое уведомление (опционально)
            const modalTrackTitle = document.getElementById('modalTrackTitle');
            const modalTrackInfo = document.getElementById('modalTrackInfo');
            if (modalTrackTitle && modalTrackInfo) {
                modalTrackTitle.innerText = `${trackName} — ${artistName}`;
                modalTrackInfo.innerText = `🎧 Открывается Яндекс Музыка в новой вкладке...`;
                // Показываем модалку на 2 секунды как уведомление
                const modal = document.getElementById('youtubeModal');
                if (modal) {
                    modal.classList.add('active');
                    document.body.style.overflow = 'hidden';
                    setTimeout(() => {
                        closeModal();
                    }, 2000);
                }
            }
        }
       
        const modal = document.getElementById('youtubeModal');
        const closeModalBtn = document.getElementById('closeModalBtn');
        const youtubeIframe = document.getElementById('youtubeIframe');
        const modalTrackTitle = document.getElementById('modalTrackTitle');
        const modalTrackInfo = document.getElementById('modalTrackInfo');
       
        function openYoutubeModal(videoIdOrUrl, trackName, artistName) {
            const embedUrl = getYoutubeEmbedUrl(videoIdOrUrl);
            if (!embedUrl) {
                alert("Не удалось распознать ссылку на YouTube. Проверьте data-youtube-id у трека.");
                return;
            }
            youtubeIframe.src = embedUrl;
            modalTrackTitle.innerText = `${trackName} — ${artistName}`;
            modalTrackInfo.innerText = `🎧 Слушайте "${trackName}" на YouTube`;
            modal.classList.add('active');
            document.body.style.overflow = 'hidden';
        }
       
        function closeModal() {
            modal.classList.remove('active');
            document.body.style.overflow = '';
            youtubeIframe.src = '';
        }
       
        if (closeModalBtn) {
            closeModalBtn.addEventListener('click', closeModal);
        }
       
        modal.addEventListener('click', (e) => {
            if (e.target === modal) {
                closeModal();
            }
        });
       
        const trackRows = document.querySelectorAll('.track-row');
        trackRows.forEach(row => {
            const playBtn = row.querySelector('.play-btn');
            if (!playBtn) return;
           
            const isYandex = row.getAttribute('data-is-yandex') === 'true';
            const yandexUrl = row.getAttribute('data-yandex-url');
            const youtubeId = row.getAttribute('data-youtube-id');
            const trackName = row.getAttribute('data-track-name');
            const artistName = row.getAttribute('data-artist');
           
            playBtn.addEventListener('click', (e) => {
                e.stopPropagation();
                row.classList.add('playing-effect');
                setTimeout(() => row.classList.remove('playing-effect'), 500);
               
                if (isYandex && yandexUrl) {
                    openYandexMusic(yandexUrl, trackName, artistName);
                } else if (youtubeId) {
                    openYoutubeModal(youtubeId, trackName, artistName);
                } else {
                    alert("Для этого трека не указан источник воспроизведения.");
                }
            });
           
            row.addEventListener('click', (e) => {
                if (e.target === playBtn || playBtn.contains(e.target)) return;
                row.classList.add('playing-effect');
                setTimeout(() => row.classList.remove('playing-effect'), 500);
               
                if (isYandex && yandexUrl) {
                    openYandexMusic(yandexUrl, trackName, artistName);
                } else if (youtubeId) {
                    openYoutubeModal(youtubeId, trackName, artistName);
                }
            });
        });
       
        const allTracks = document.querySelectorAll('.track-row');
        allTracks.forEach((track, idx) => {
            track.style.opacity = '0';
            track.style.transform = 'translateY(6px)';
            track.style.transition = `opacity 0.2s cubic-bezier(0.2, 0.9, 0.4, 1) ${idx * 0.02}s, transform 0.25s ease ${idx * 0.02}s`;
            setTimeout(() => {
                track.style.opacity = '1';
                track.style.transform = 'translateY(0)';
            }, 50);
        });
       
        document.addEventListener('keydown', (e) => {
            if (e.key === 'Escape' && modal.classList.contains('active')) {
                closeModal();
            }
        });
    })();
</script>
</body>
</html>[/html]

0

218

[html]<!DOCTYPE html>
<html lang="ru">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=yes">
    <title>ракшор · цирк уродов · плейлист</title>
    <style>
        * {
            margin: 0;
            padding: 0;
            box-sizing: border-box;
        }

        body {
            background: #0a0308;
            display: flex;
            align-items: center;
            justify-content: center;
            min-height: 100vh;
            font-family: 'Inter', -apple-system, 'Segoe UI', 'SF Pro Text', 'Poppins', system-ui, sans-serif;
            padding: 2rem;
        }

        .forum-post {
            max-width: 980px;
            width: 100%;
            margin: 0 auto;
            margin-top: 3rem;
        }

        /* КАРТОЧКА С ЦИРКОВЫМ ГРАДИЕНТОМ И АНИМИРОВАННЫМИ ПОЛОСАМИ */
        .playlist-card {
            background: #0F0B10;
            border-radius: 32px;
            overflow: hidden;
            border: 1px solid rgba(230, 180, 50, 0.3);
            box-shadow: 0 0 20px rgba(200, 100, 200, 0.15);
            transition: none;
            position: relative;
        }

        /* Анимированные цирковые полосы (фон) */
        .playlist-card::before {
            content: "";
            position: absolute;
            top: 0;
            left: 0;
            width: 100%;
            height: 100%;
            background: repeating-linear-gradient(
                45deg,
                rgba(180, 60, 180, 0.08) 0px,
                rgba(180, 60, 180, 0.08) 20px,
                rgba(230, 180, 50, 0.05) 20px,
                rgba(230, 180, 50, 0.05) 40px
            );
            pointer-events: none;
            z-index: 0;
            animation: stripesMove 20s linear infinite;
        }

        @keyframes stripesMove {
            0% { background-position: 0 0; }
            100% { background-position: 80px 80px; }
        }

        .hero-gradient-bg {
            background: linear-gradient(145deg, #2a1528 0%, #0F0B10 100%);
            border-radius: 32px 32px 0 0;
            padding: 2rem 2rem 1rem 2rem;
            margin: -2rem -2rem 0 -2rem;
            position: relative;
            z-index: 1;
            border-bottom: 2px solid #e6b432;
        }

        .playlist-inner {
            padding: 2rem 2rem 2rem 2rem;
            position: relative;
            z-index: 1;
        }

        .hero-section {
            display: flex;
            align-items: center;
            gap: 2rem;
            flex-wrap: wrap;
            margin-bottom: 0.5rem;
        }

        .avatar-huge {
            width: 170px;
            height: 170px;
            flex-shrink: 0;
            border-radius: 2px;
            object-fit: cover;
            border: 2px solid #e6b432;
            box-shadow: 0 0 15px rgba(230, 180, 50, 0.5);
            transition: all 0.2s ease;
            background: #2A1528;
        }

        .avatar-huge:hover {
            transform: scale(1.01);
            border-color: #ffcc44;
            box-shadow: 0 0 25px rgba(230, 180, 50, 0.8);
        }

        .title-group {
            flex: 1;
        }

        .main-title {
            font-size: 3.4rem;
            font-weight: 800;
            letter-spacing: -1px;
            background: linear-gradient(135deg, #e6b432 0%, #cc88ff 100%);
            background-clip: text;
            -webkit-background-clip: text;
            color: transparent;
            text-shadow: 0 0 10px rgba(230, 180, 50, 0.3);
        }

        .playlist-sub {
            font-size: 1rem;
            color: #d4b880;
            margin-bottom: 0.3rem;
            font-weight: 450;
            letter-spacing: -0.2px;
        }

        .track-stats {
            font-size: 0.85rem;
            font-family: monospace;
            color: #c9a563;
            border-bottom: 1px solid #4a2a40;
            padding-bottom: 1rem;
            margin-bottom: 1.25rem;
            display: flex;
            gap: 0.75rem;
            margin-top: 0.25rem;
        }

        .track-stats span {
            background: #2a1528;
            padding: 0.2rem 0.8rem;
            border-radius: 30px;
            font-size: 0.75rem;
            font-family: monospace;
            letter-spacing: 0.3px;
            color: #e6b432;
            border: 1px solid #e6b43233;
        }

        .tracklist-container {
            max-height: 460px;
            overflow-y: auto;
            padding-right: 6px;
            margin-right: -6px;
        }

        .tracklist {
            display: flex;
            flex-direction: column;
            gap: 0px;
        }

        .track-row {
            display: flex;
            align-items: center;
            justify-content: space-between;
            padding: 0.9rem 0.5rem 0.9rem 0;
            border-bottom: 1px solid #3a2035;
            transition: background 0.2s ease;
            cursor: pointer;
        }

        .track-left {
            display: flex;
            align-items: baseline;
            gap: 1rem;
            flex-wrap: wrap;
            flex: 1;
        }

        .track-number {
            font-weight: 600;
            font-family: monospace;
            font-size: 1rem;
            color: #b8884a;
            width: 32px;
            transition: color 0.2s;
        }

        .track-info {
            display: flex;
            flex-direction: column;
        }

        .track-name {
            font-weight: 600;
            font-size: 1rem;
            color: #ecd9b4;
            letter-spacing: -0.2px;
        }

        .track-artist {
            font-size: 0.7rem;
            color: #b87a6a;
            margin-top: 2px;
        }

        .track-duration {
            font-family: monospace;
            font-size: 0.85rem;
            color: #c9a563;
            background: transparent;
            padding: 0.2rem 0;
            transition: all 0.2s;
            display: flex;
            align-items: center;
            gap: 8px;
        }

        .play-btn {
            background: rgba(230, 180, 50, 0.2);
            border: 1px solid rgba(230, 180, 50, 0.4);
            border-radius: 30px;
            padding: 0.25rem 0.9rem;
            font-size: 0.7rem;
            font-weight: 500;
            font-family: monospace;
            color: #e6b432;
            cursor: pointer;
            transition: all 0.2s ease;
            backdrop-filter: blur(4px);
            letter-spacing: 0.3px;
        }

        .play-btn:hover {
            background: #e6b432;
            color: #1a0a18;
            transform: scale(1.02);
            box-shadow: 0 0 12px rgba(230, 180, 50, 0.6);
        }

        .playing-effect {
            animation: pulsePurple 0.5s ease-out;
            border-radius: 20px;
        }

        @keyframes pulsePurple {
            0% { background: rgba(180, 60, 180, 0); }
            50% { background: rgba(180, 60, 180, 0.3); }
            100% { background: rgba(180, 60, 180, 0); }
        }

        .track-row:hover {
            background: #1f0f1c;
            border-radius: 18px;
            padding-left: 12px;
            padding-right: 12px;
            margin-left: -12px;
            margin-right: -6px;
            border-bottom-color: #e6b432;
        }

        .track-row:hover .track-number {
            color: #e6b432;
            text-shadow: 0 0 6px rgba(230, 180, 50, 0.6);
        }

        .track-row:hover .track-name {
            color: #ffeaac;
        }

        .track-row:hover .track-artist {
            color: #e6b432;
        }

        .track-row:hover .track-duration {
            color: #ffdd99;
        }

        /* МОДАЛКА В СТИЛЕ ЦИРК */
        .youtube-modal {
            position: fixed;
            top: 0;
            left: 0;
            width: 100%;
            height: 100%;
            background-color: rgba(30, 10, 30, 0.9);
            backdrop-filter: blur(12px);
            z-index: 2000;
            display: flex;
            align-items: center;
            justify-content: center;
            opacity: 0;
            visibility: hidden;
            transition: opacity 0.3s ease, visibility 0s linear 0.3s;
        }

        .youtube-modal.active {
            opacity: 1;
            visibility: visible;
            transition: opacity 0.3s ease, visibility 0s linear 0s;
        }

        .modal-container {
            background: #1a0a1a;
            border-radius: 32px;
            width: 90%;
            max-width: 800px;
            overflow: hidden;
            box-shadow: 0 0 40px rgba(230, 180, 50, 0.3), 0 0 0 2px #e6b432;
            transform: scale(0.96);
            transition: transform 0.25s cubic-bezier(0.2, 0.95, 0.4, 1.05);
        }

        .active .modal-container {
            transform: scale(1);
        }

        .modal-header {
            display: flex;
            justify-content: space-between;
            align-items: center;
            padding: 1rem 1.5rem;
            background: #2a1528;
            border-bottom: 2px solid #e6b432;
        }

        .modal-title {
            font-weight: 600;
            color: #e6b432;
            font-size: 1rem;
            text-shadow: 0 0 5px rgba(230, 180, 50, 0.5);
        }

        .close-modal {
            font-size: 28px;
            font-weight: 300;
            color: #e6b432;
            cursor: pointer;
            transition: color 0.2s;
            line-height: 1;
        }

        .close-modal:hover {
            color: #ffcc66;
        }

        .modal-body {
            padding: 1.5rem;
            background: #1a0a1a;
        }

        .youtube-iframe-wrapper {
            position: relative;
            width: 100%;
            padding-bottom: 56.25%;
            height: 0;
            border-radius: 20px;
            overflow: hidden;
            border: 1px solid #e6b432;
        }

        .youtube-iframe-wrapper iframe {
            position: absolute;
            top: 0;
            left: 0;
            width: 100%;
            height: 100%;
            border: 0;
            border-radius: 16px;
        }

        .track-info-modal {
            margin-top: 1rem;
            text-align: center;
            color: #d4b880;
            font-size: 0.85rem;
        }

        .modal-play-btn {
            background: #e6b432;
            border: none;
            border-radius: 30px;
            padding: 0.5rem 1.2rem;
            font-size: 0.8rem;
            font-weight: 600;
            color: #1a0a1a;
            cursor: pointer;
            transition: all 0.2s ease;
            margin-top: 0.8rem;
            font-family: monospace;
            box-shadow: 0 0 10px rgba(230, 180, 50, 0.5);
        }

        .modal-play-btn:hover {
            background: #ffcc44;
            transform: scale(1.02);
            box-shadow: 0 0 18px rgba(230, 180, 50, 0.8);
        }

        .tracklist-container::-webkit-scrollbar {
            width: 4px;
        }
        .tracklist-container::-webkit-scrollbar-track {
            background: #2a1528;
            border-radius: 10px;
        }
        .tracklist-container::-webkit-scrollbar-thumb {
            background: #e6b432;
            border-radius: 10px;
        }

        @media (max-width: 720px) {
            .playlist-inner {
                padding: 1.3rem;
            }
            .hero-gradient-bg {
                padding: 1.3rem 1.3rem 0.5rem 1.3rem;
                margin: -1.3rem -1.3rem 0 -1.3rem;
            }
            .avatar-huge {
                width: 110px;
                height: 110px;
            }
            .main-title {
                font-size: 2.2rem;
            }
            .track-row {
                padding: 0.7rem 0;
            }
            .track-left {
                gap: 0.7rem;
            }
            .track-number {
                width: 28px;
                font-size: 0.9rem;
            }
            .forum-post {
                margin-top: 2rem;
            }
        }
    </style>
</head>
<body>
<div class="forum-post">
    <div class="playlist-card">
        <div class="playlist-inner">
            <div class="hero-gradient-bg">
                <div class="hero-section">
                    <img id="characterAvatar" class="avatar-huge" src="https://upforme.ru/uploads/0017/24/ab/2/803846.jpg" alt="avatar">
                    <div class="title-group">
                        <div class="main-title">молли</div>
                        <div class="playlist-sub">плейлист<br>we're carnival people, we've all got our issues. you don't end up here if you're not a little weird.</div>
                    </div>
                </div>
            </div>

            <div class="track-stats">
                <span>🎪 5 треков 🎪</span> <span>шапито</span>
            </div>

            <div class="tracklist-container">
                <div class="tracklist">
                    <!-- 1. Seven Devils - Florence and the Machine -->
                    <div class="track-row" data-track-name="Seven Devils" data-artist="Florence and the Machine" data-youtube-id="yJL5SE1i0u4">
                        <div class="track-left">
                            <span class="track-number">1</span>
                            <div class="track-info">
                                <div class="track-name">Seven Devils</div>
                                <div class="track-artist">Florence and the Machine</div>
                            </div>
                        </div>
                        <div class="track-duration">
                            <button class="play-btn">▶ слушать</button>
                        </div>
                    </div>
                    <!-- 2. Brighter - Christian Borle, Sam Haft & Andrew Underberg -->
                    <div class="track-row" data-track-name="Brighter" data-artist="Christian Borle, Sam Haft & Andrew Underberg" data-youtube-id="bIGCddo_WkY">
                        <div class="track-left">
                            <span class="track-number">2</span>
                            <div class="track-info">
                                <div class="track-name">Brighter</div>
                                <div class="track-artist">Christian Borle, Sam Haft & Andrew Underberg</div>
                            </div>
                        </div>
                        <div class="track-duration">
                            <button class="play-btn">▶ слушать</button>
                        </div>
                    </div>
                    <!-- 3. The Other Side - Hugh Jackman & Zac Efron -->
                    <div class="track-row" data-track-name="The Other Side" data-artist="Hugh Jackman & Zac Efron" data-youtube-id="Wk008ADh4iY">
                        <div class="track-left">
                            <span class="track-number">3</span>
                            <div class="track-info">
                                <div class="track-name">The Other Side</div>
                                <div class="track-artist">Hugh Jackman & Zac Efron</div>
                            </div>
                        </div>
                        <div class="track-duration">
                            <button class="play-btn">▶ слушать</button>
                        </div>
                    </div>
                    <!-- 4. Carnival - John Michael Howell -->
                    <div class="track-row" data-track-name="Carnival" data-artist="John Michael Howell" data-youtube-id="8PPu3fyUfT4">
                        <div class="track-left">
                            <span class="track-number">4</span>
                            <div class="track-info">
                                <div class="track-name">Carnival</div>
                                <div class="track-artist">John Michael Howell</div>
                            </div>
                        </div>
                        <div class="track-duration">
                            <button class="play-btn">▶ слушать</button>
                        </div>
                    </div>
                    <!-- 5. Could Have Been Me - The Struts -->
                    <div class="track-row" data-track-name="Could Have Been Me" data-artist="The Struts" data-youtube-id="ARhk9K_mviE">
                        <div class="track-left">
                            <span class="track-number">5</span>
                            <div class="track-info">
                                <div class="track-name">Could Have Been Me</div>
                                <div class="track-artist">The Struts</div>
                            </div>
                        </div>
                        <div class="track-duration">
                            <button class="play-btn">▶ слушать</button>
                        </div>
                    </div>
                </div>
            </div>

            <div style="text-align: center; font-size: 0.7rem; color: #c9a563; margin-top: 16px; font-family: monospace;">
                🎪 нажми «слушать» — откроется YouTube плеер 🎪
            </div>
        </div>
    </div>
</div>

<div id="youtubeModal" class="youtube-modal">
    <div class="modal-container">
        <div class="modal-header">
            <div class="modal-title" id="modalTrackTitle">Сейчас играет</div>
            <div class="close-modal" id="closeModalBtn">&times;</div>
        </div>
        <div class="modal-body">
            <div class="youtube-iframe-wrapper">
                <iframe id="youtubeIframe" src="" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture" allowfullscreen></iframe>
            </div>
            <div class="track-info-modal" id="modalTrackInfo"></div>
            <div style="text-align: center;">
                <button id="modalPlayBtn" class="modal-play-btn">🎪 ВОСПРОИЗВЕСТИ 🎪</button>
            </div>
        </div>
    </div>
</div>

<script>
    (function() {
        function extractYoutubeId(urlOrId) {
            if (!urlOrId) return null;
            if (urlOrId.length >= 8 && !urlOrId.includes('/') && !urlOrId.includes('youtube.com') && !urlOrId.includes('youtu.be')) {
                return urlOrId;
            }
            const patterns = [
                /(?:youtube\.com\/watch\?v=|youtu\.be\/|youtube\.com\/embed\/)([a-zA-Z0-9_-]{11})(?:[?&]|$)/,
                /^([a-zA-Z0-9_-]{11})$/
            ];
            for (let pattern of patterns) {
                const match = urlOrId.match(pattern);
                if (match && match[1]) {
                    return match[1];
                }
            }
            return null;
        }

        function getYoutubeEmbedUrl(youtubeIdOrUrl) {
            const videoId = extractYoutubeId(youtubeIdOrUrl);
            if (videoId) {
                return `https://www.youtube.com/embed/${videoId}?enablejsapi=1`;
            }
            return null;
        }
       
        const modal = document.getElementById('youtubeModal');
        const closeModalBtn = document.getElementById('closeModalBtn');
        const youtubeIframe = document.getElementById('youtubeIframe');
        const modalTrackTitle = document.getElementById('modalTrackTitle');
        const modalTrackInfo = document.getElementById('modalTrackInfo');
        const modalPlayBtn = document.getElementById('modalPlayBtn');
       
        function openYoutubeModal(videoIdOrUrl, trackName, artistName) {
            const embedUrl = getYoutubeEmbedUrl(videoIdOrUrl);
            if (!embedUrl) {
                alert("Не удалось распознать ссылку на YouTube.");
                return;
            }
            youtubeIframe.src = embedUrl;
            modalTrackTitle.innerText = `${trackName} — ${artistName}`;
            modalTrackInfo.innerText = `🎪 Нажмите кнопку ниже, чтобы начать представление 🎪`;
            modal.classList.add('active');
            document.body.style.overflow = 'hidden';
        }
       
        function playCurrentVideo() {
            if (youtubeIframe && youtubeIframe.contentWindow) {
                youtubeIframe.contentWindow.postMessage('{"event":"command","func":"playVideo","args":""}', '*');
            }
        }
       
        function closeModal() {
            modal.classList.remove('active');
            document.body.style.overflow = '';
            youtubeIframe.src = '';
        }
       
        if (closeModalBtn) {
            closeModalBtn.addEventListener('click', closeModal);
        }
       
        if (modalPlayBtn) {
            modalPlayBtn.addEventListener('click', playCurrentVideo);
        }
       
        modal.addEventListener('click', (e) => {
            if (e.target === modal) {
                closeModal();
            }
        });
       
        const trackRows = document.querySelectorAll('.track-row');
        trackRows.forEach(row => {
            const playBtn = row.querySelector('.play-btn');
            if (!playBtn) return;
           
            const youtubeId = row.getAttribute('data-youtube-id');
            const trackName = row.getAttribute('data-track-name');
            const artistName = row.getAttribute('data-artist');
           
            playBtn.addEventListener('click', (e) => {
                e.stopPropagation();
                row.classList.add('playing-effect');
                setTimeout(() => row.classList.remove('playing-effect'), 500);
               
                if (youtubeId) {
                    openYoutubeModal(youtubeId, trackName, artistName);
                } else {
                    alert("Для этого трека не указан источник воспроизведения.");
                }
            });
           
            row.addEventListener('click', (e) => {
                if (e.target === playBtn || playBtn.contains(e.target)) return;
                row.classList.add('playing-effect');
                setTimeout(() => row.classList.remove('playing-effect'), 500);
                if (youtubeId) {
                    openYoutubeModal(youtubeId, trackName, artistName);
                }
            });
        });
       
        const allTracks = document.querySelectorAll('.track-row');
        allTracks.forEach((track, idx) => {
            track.style.opacity = '0';
            track.style.transform = 'translateY(6px)';
            track.style.transition = `opacity 0.2s cubic-bezier(0.2, 0.9, 0.4, 1) ${idx * 0.02}s, transform 0.25s ease ${idx * 0.02}s`;
            setTimeout(() => {
                track.style.opacity = '1';
                track.style.transform = 'translateY(0)';
            }, 50);
        });
       
        document.addEventListener('keydown', (e) => {
            if (e.key === 'Escape' && modal.classList.contains('active')) {
                closeModal();
            }
        });
    })();
</script>
</body>
</html>[/html]

0

219

[html]<!DOCTYPE html>
<html>
<head>
    <meta charset="UTF-8">
    <title>Бункер | Карточка игрока (привязка по нику)</title>
    <style>
        * {
            box-sizing: border-box;
        }
        body {
            background: #1e2a1a;
            font-family: 'Segoe UI', 'Roboto', system-ui, sans-serif;
            display: flex;
            justify-content: center;
            align-items: center;
            padding: 30px 16px;
            margin: 0;
        }
        .bunker-container {
            max-width: 1100px;
            width: 100%;
        }
        .master-panel {
            background: #2f2b1fe0;
            backdrop-filter: blur(2px);
            border-radius: 48px;
            padding: 16px 24px;
            margin-bottom: 24px;
            display: flex;
            flex-wrap: wrap;
            gap: 16px;
            align-items: flex-end;
            justify-content: space-between;
        }
        .master-controls {
            display: flex;
            gap: 16px;
            flex-wrap: wrap;
            align-items: flex-end;
        }
        .master-controls div {
            display: flex;
            flex-direction: column;
            gap: 5px;
        }
        .master-controls label {
            color: #ffddb0;
            font-weight: bold;
            font-size: 0.7rem;
        }
        .master-controls input {
            background: #fff7ea;
            border: 1px solid #bf9a60;
            border-radius: 60px;
            padding: 8px 16px;
            font-family: monospace;
            width: 220px;
        }
        button {
            background: #598b3a;
            border: none;
            padding: 8px 20px;
            border-radius: 40px;
            font-weight: bold;
            color: #faf0cf;
            cursor: pointer;
            box-shadow: 0 3px 0 #2d4a1a;
            transition: 0.05s linear;
        }
        button:active {
            transform: translateY(2px);
            box-shadow: 0 1px 0 #2d4a1a;
        }
        .user-info {
            background: #1e2a1a;
            padding: 8px 18px;
            border-radius: 40px;
            color: #ffdd99;
            font-family: monospace;
        }
        .user-info span {
            color: #ffb347;
            font-weight: bold;
        }
        .player-card {
            background: #fdf2e0;
            border-radius: 40px;
            border: 2px solid #cb9a5e;
            box-shadow: 12px 12px 0 rgba(0,0,0,0.2);
            padding: 24px;
        }
        .identity-row {
            display: flex;
            gap: 24px;
            align-items: center;
            margin-bottom: 28px;
            flex-wrap: wrap;
            border-bottom: 2px dashed #dbb87a;
            padding-bottom: 20px;
        }
        .avatar {
            width: 85px;
            height: 85px;
            background: #7f5a38;
            border-radius: 24px;
            display: flex;
            align-items: center;
            justify-content: center;
            font-size: 3.2rem;
            box-shadow: inset 0 0 0 3px #fbe5b9, 0 6px 0 #33200c;
        }
        .nick-block {
            flex: 1;
        }
        .nick-label {
            font-size: 0.7rem;
            letter-spacing: 2px;
            color: #b47c44;
            font-weight: bold;
        }
        .player-nick {
            font-size: 1.8rem;
            font-weight: 800;
            font-family: monospace;
            background: #2a241a;
            display: inline-block;
            padding: 0 16px;
            border-radius: 60px;
            color: #ffda99;
        }
        .stats-grid {
            display: grid;
            grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
            gap: 16px;
        }
        .stat-card {
            background: #f9efdc;
            border-radius: 28px;
            border: 1px solid #e2c394;
            overflow: hidden;
        }
        .stat-header {
            background: #dbb067;
            padding: 8px 14px;
            font-weight: bold;
            font-size: 0.75rem;
            text-transform: uppercase;
            color: #2a2418;
            display: flex;
            justify-content: space-between;
        }
        .stat-value {
            padding: 16px 14px;
            min-height: 80px;
            background: #fff9ef;
            font-size: 0.95rem;
            word-break: break-word;
            cursor: pointer;
            transition: 0.05s;
            display: flex;
            align-items: center;
            justify-content: space-between;
        }
        .stat-value.closed {
            background: #e7d9c2;
            color: #6f5237;
            font-style: italic;
            justify-content: center;
        }
        .stat-value.opened {
            background: #fff2e2;
            border-left: 5px solid #719e3a;
            font-weight: 500;
            color: #1f2e0c;
        }
        .lock-icon {
            font-size: 0.7rem;
            background: #493a28;
            padding: 2px 8px;
            border-radius: 40px;
            color: #ffd89a;
        }
        .footer-info {
            margin-top: 24px;
            text-align: center;
            font-size: 0.7rem;
            background: #fff0db;
            border-radius: 30px;
            padding: 10px;
        }
        .editor-area {
            margin-top: 20px;
            background: #2e2a1c;
            border-radius: 32px;
            padding: 12px 18px;
        }
        .editor-toggle {
            cursor: pointer;
            color: #ffd89a;
            font-weight: bold;
            display: flex;
            justify-content: space-between;
        }
        .editor-content {
            display: none;
            margin-top: 15px;
        }
        textarea {
            width: 100%;
            background: #fff2e0;
            border-radius: 20px;
            border: 1px solid #b68b48;
            padding: 8px 12px;
            font-family: monospace;
        }
        @media (max-width: 650px) {
            .stats-grid { grid-template-columns: 1fr; }
            .player-nick { font-size: 1.3rem; }
        }
    </style>
</head>
<body>
<div class="bunker-container">
    <div class="master-panel">
        <div class="user-info" id="userInfoBlock">
            🔐 <span id="userStatusText">Определение вашего ника...</span>
        </div>
        <div class="master-controls">
            <div>
                <label>👤 НИК ВЛАДЕЛЬЦА (кому карточка)</label>
                <input type="text" id="ownerNickInput" placeholder="Например: Mollymauk Tealeaf" value="Mollymauk Tealeaf">
            </div>
            <div>
                <label>🖼️ Аватар (эмодзи)</label>
                <input type="text" id="avatarInput" value="🃏">
            </div>
            <button id="applyIdentityBtn">🎲 ПРИВЯЗАТЬ КАРТОЧКУ</button>
        </div>
    </div>

    <div id="cardRoot"></div>
    <div class="editor-area" id="editorPanel"></div>
   
    <div class="footer-info">
        🔐 <strong>Привязка по нику форума!</strong> Только игрок, вошедший под ником <strong id="ownerNickDisplay">???</strong>, может открывать характеристики.<br>
        Ваш текущий ник: <strong id="currentUserNickDisplay">???</strong> — если он совпадает с ником владельца, вы можете вскрывать данные.
    </div>
</div>

<script>
    // ==================== ОПРЕДЕЛЕНИЕ НИКА ПОЛЬЗОВАТЕЛЯ MYBB через класс UserLogin ====================
    let currentUserNick = null;
   
    function getMyBBNickname() {
        // ПЕРВЫЙ ПРИОРИТЕТ: ищем элемент с классом UserLogin (как ты сказал)
        const loginElem = document.querySelector('.UserLogin');
        if (loginElem) {
            let nick = loginElem.textContent.trim();
            if (nick) {
                console.log('[Бункер] Ник найден через .UserLogin:', nick);
                return nick;
            }
        }
       
        // ВТОРОЙ ПРИОРИТЕТ: глобальные переменные MyBB
        if (typeof mybb !== 'undefined' && mybb.user && mybb.user.username) {
            console.log('[Бункер] Ник найден через mybb.user.username:', mybb.user.username);
            return mybb.user.username;
        }
        if (typeof username !== 'undefined' && username) {
            console.log('[Бункер] Ник найден через username:', username);
            return username;
        }
       
        // ТРЕТИЙ ПРИОРИТЕТ: другие возможные классы на форуме
        const altSelectors = [
            '.profile-name', '.username', '.user-name',
            '.author strong', '.postbit .username'
        ];
        for (let selector of altSelectors) {
            const elem = document.querySelector(selector);
            if (elem) {
                let nick = elem.textContent.trim();
                if (nick && nick.length > 0 && nick.length < 50) {
                    console.log('[Бункер] Ник найден через селектор', selector + ':', nick);
                    return nick;
                }
            }
        }
       
        console.log('[Бункер] Не удалось определить ник автоматически');
        return null;
    }
   
    currentUserNick = getMyBBNickname();
   
    // Если не определилось — запрашиваем один раз
    if (!currentUserNick) {
        const savedNick = localStorage.getItem('bunker_user_nick');
        if (savedNick) {
            currentUserNick = savedNick;
            console.log('[Бункер] Ник загружен из localStorage:', currentUserNick);
        } else {
            let entered = prompt("🔐 Система не смогла автоматически определить ваш ник на форуме.\nВведите ваш логин (никнейм) один раз:", "");
            if (entered && entered.trim()) {
                currentUserNick = entered.trim();
                localStorage.setItem('bunker_user_nick', currentUserNick);
                console.log('[Бункер] Ник введён вручную:', currentUserNick);
            }
        }
    }
   
    // Отображение статуса
    function updateUserStatus() {
        const statusSpan = document.getElementById('userStatusText');
        const displaySpan = document.getElementById('currentUserNickDisplay');
        if (statusSpan) {
            if (currentUserNick) {
                statusSpan.innerHTML = `✅ Вы вошли как: ${currentUserNick}`;
            } else {
                statusSpan.innerHTML = `⚠️ Ник не определён. Обновите страницу или введите вручную.`;
            }
        }
        if (displaySpan) {
            displaySpan.textContent = currentUserNick || "не определён";
        }
    }
    updateUserStatus();
   
    // ==================== ДАННЫЕ КАРТОЧКИ ====================
    const STATS_LIST = [
        { id: "basic",     label: "🧬 Пол, возраст, плодовитость, стаж" },
        { id: "profession",label: "🔧 Профессия" },
        { id: "health",    label: "❤️ Здоровье" },
        { id: "bio",       label: "🧬 Биологические особенности" },
        { id: "hobby",     label: "🎨 Хобби / увлечение" },
        { id: "baggage",   label: "🎒 Багаж / Инвентарь" },
        { id: "phobia",    label: "😨 Фобии" },
        { id: "fact1",     label: "📖 Факт 1" },
        { id: "fact2",     label: "📖 Факт 2" }
    ];
   
    let cardOwnerNick = "Mollymauk Tealeaf";
    let cardAvatar = "🃏";
   
    let cardStats = {
        basic:      "Мужской, 28 лет, высокая плодовитость, стаж 5 лет",
        profession: "Карнавальный артист, жонглёр",
        health:     "Здоров, но есть шрамы",
        bio:        "Гибкий, ловкий, яркая внешность",
        hobby:      "Тарология, карточные фокусы",
        baggage:    "Колода карт, маска, бутылка виски",
        phobia:     "Боязнь тишины",
        fact1:      "Умеет читать мысли по картам",
        fact2:      "Никогда не снимает перчатки"
    };
   
    let revealedStats = {
        basic: false, profession: false, health: false, bio: false,
        hobby: false, baggage: false, phobia: false, fact1: false, fact2: false
    };
   
    function loadCard() {
        const saved = localStorage.getItem('bunker_card');
        if (saved) {
            try {
                const data = JSON.parse(saved);
                if (data.cardOwnerNick) cardOwnerNick = data.cardOwnerNick;
                if (data.cardAvatar) cardAvatar = data.cardAvatar;
                if (data.cardStats) cardStats = { ...cardStats, ...data.cardStats };
                if (data.revealedStats) revealedStats = { ...revealedStats, ...data.revealedStats };
            } catch(e) {}
        }
        const nickInput = document.getElementById('ownerNickInput');
        const avatarInput = document.getElementById('avatarInput');
        if (nickInput) nickInput.value = cardOwnerNick;
        if (avatarInput) avatarInput.value = cardAvatar;
        const nickDisplay = document.getElementById('ownerNickDisplay');
        if (nickDisplay) nickDisplay.textContent = cardOwnerNick;
    }
   
    function saveCard() {
        const data = { cardOwnerNick, cardAvatar, cardStats, revealedStats };
        localStorage.setItem('bunker_card', JSON.stringify(data));
    }
   
    function syncFromInputs() {
        const newNick = document.getElementById('ownerNickInput').value.trim();
        if (newNick) cardOwnerNick = newNick;
        const newAvatar = document.getElementById('avatarInput').value.trim();
        if (newAvatar) cardAvatar = newAvatar;
        saveCard();
        const nickDisplay = document.getElementById('ownerNickDisplay');
        if (nickDisplay) nickDisplay.textContent = cardOwnerNick;
    }
   
    function escapeHtml(str) {
        if (!str) return '';
        return str.replace(/[&<>]/g, function(m) {
            if (m === '&') return '&amp;';
            if (m === '<') return '&lt;';
            if (m === '>') return '&gt;';
            return m;
        });
    }
   
    function renderCard() {
        const container = document.getElementById('cardRoot');
        if (!container) return;
       
        let statsHtml = '';
        for (let stat of STATS_LIST) {
            const isOpen = revealedStats[stat.id];
            const statText = cardStats[stat.id] || "—";
            if (isOpen) {
                statsHtml += `
                    <div class="stat-card">
                        <div class="stat-header"><span>${stat.label}</span><span>🔓 открыто</span></div>
                        <div class="stat-value opened" data-stat-id="${stat.id}">✨ ${escapeHtml(statText)} ✨</div>
                    </div>
                `;
            } else {
                statsHtml += `
                    <div class="stat-card">
                        <div class="stat-header"><span>${stat.label}</span><span>🔒 закрыто</span></div>
                        <div class="stat-value closed" data-stat-id="${stat.id}">❓ НАЖМИТЕ, ЧТОБЫ ВСКРЫТЬ ❓<span class="lock-icon">закрыто</span></div>
                    </div>
                `;
            }
        }
       
        container.innerHTML = `
            <div class="player-card">
                <div class="identity-row">
                    <div class="avatar">${escapeHtml(cardAvatar)}</div>
                    <div class="nick-block">
                        <div class="nick-label">ВЛАДЕЛЕЦ КАРТОЧКИ</div>
                        <div class="player-nick">${escapeHtml(cardOwnerNick)}</div>
                    </div>
                </div>
                <div class="stats-grid" id="statsGridLive">${statsHtml}</div>
            </div>
        `;
        attachClickHandlers();
    }
   
    function attachClickHandlers() {
        document.querySelectorAll('.stat-value').forEach(div => {
            div.removeEventListener('click', statHandler);
            div.addEventListener('click', statHandler);
        });
    }
   
    function statHandler(e) {
        const target = e.currentTarget;
        const statId = target.getAttribute('data-stat-id');
        if (!statId) return;
       
        if (revealedStats[statId]) {
            alert(`🔓 Эта характеристика уже открыта!\n\n${cardStats[statId]}`);
            return;
        }
       
        // ПРОВЕРКА: текущий пользователь — владелец карточки?
        if (!currentUserNick) {
            let entered = prompt("🔐 Система не определила ваш ник. Введите ваш логин на форуме:", "");
            if (entered && entered.trim()) {
                currentUserNick = entered.trim();
                localStorage.setItem('bunker_user_nick', currentUserNick);
                updateUserStatus();
            } else {
                alert("❌ Нужно ввести ник для проверки прав.");
                return;
            }
        }
       
        if (currentUserNick !== cardOwnerNick) {
            alert(`❌ ДОСТУП ЗАПРЕЩЁН!\nВы вошли как: "${currentUserNick}"\nЭта карточка принадлежит: "${cardOwnerNick}"\nОткрыть характеристики может только владелец карточки.`);
            return;
        }
       
        // Открываем
        revealedStats[statId] = true;
        saveCard();
        renderCard();
        alert(`✅ Игрок ${cardOwnerNick}, вы открыли характеристику:\n"${cardStats[statId]}"\nТеперь она видна всем.`);
    }
   
    function buildEditorPanel() {
        const panel = document.getElementById('editorPanel');
        if (!panel) return;
       
        panel.innerHTML = `
            <div class="editor-toggle" id="toggleEditorBtn">
                <strong>🛠️ РЕДАКТОР ХАРАКТЕРИСТИК (ведущий)</strong>
                <span>▼</span>
            </div>
            <div class="editor-content" id="editorContent">
                ${STATS_LIST.map(stat => `
                    <div style="margin-bottom: 12px;">
                        <label style="color:#ffcf87; display:block;">${stat.label}</label>
                        <textarea id="edit_${stat.id}" rows="2">${escapeHtml(cardStats[stat.id] || '')}</textarea>
                    </div>
                `).join('')}
                <div style="display: flex; gap: 12px; flex-wrap: wrap;">
                    <button id="saveStatsBtn">💾 СОХРАНИТЬ</button>
                    <button id="resetRevealsBtn">🔒 СБРОСИТЬ ОТКРЫТИЯ</button>
                    <button id="resetAllBtn" style="background:#8b5a3a;">🗑️ СБРОСИТЬ ВСЁ</button>
                </div>
            </div>
        `;
       
        const toggle = document.getElementById('toggleEditorBtn');
        const editorDiv = document.getElementById('editorContent');
        toggle.addEventListener('click', () => {
            if (editorDiv.style.display === 'none') {
                editorDiv.style.display = 'block';
                STATS_LIST.forEach(s => {
                    const ta = document.getElementById(`edit_${s.id}`);
                    if (ta) ta.value = cardStats[s.id] || '';
                });
            } else {
                editorDiv.style.display = 'none';
            }
        });
       
        document.getElementById('saveStatsBtn')?.addEventListener('click', () => {
            STATS_LIST.forEach(s => {
                const ta = document.getElementById(`edit_${s.id}`);
                if (ta) cardStats[s.id] = ta.value;
            });
            saveCard();
            renderCard();
            alert('✅ Характеристики обновлены');
        });
       
        document.getElementById('resetRevealsBtn')?.addEventListener('click', () => {
            if (confirm('Сбросить все открытия?')) {
                STATS_LIST.forEach(s => revealedStats[s.id] = false);
                saveCard();
                renderCard();
                alert('🔒 Все характеристики снова скрыты');
            }
        });
       
        document.getElementById('resetAllBtn')?.addEventListener('click', () => {
            if (confirm('ПОЛНЫЙ СБРОС карточки? Все данные вернутся к стандартным.')) {
                localStorage.removeItem('bunker_card');
                cardOwnerNick = document.getElementById('ownerNickInput')?.value.trim() || "Mollymauk Tealeaf";
                cardAvatar = "🃏";
                cardStats = {
                    basic: "Мужской, 28 лет, высокая плодовитость, стаж 5 лет",
                    profession: "Карнавальный артист, жонглёр",
                    health: "Здоров, но есть шрамы",
                    bio: "Гибкий, ловкий, яркая внешность",
                    hobby: "Тарология, карточные фокусы",
                    baggage: "Колода карт, маска, бутылка виски",
                    phobia: "Боязнь тишины",
                    fact1: "Умеет читать мысли по картам",
                    fact2: "Никогда не снимает перчатки"
                };
                STATS_LIST.forEach(s => revealedStats[s.id] = false);
                saveCard();
                document.getElementById('ownerNickInput').value = cardOwnerNick;
                document.getElementById('avatarInput').value = cardAvatar;
                renderCard();
                alert('💥 Карточка сброшена');
            }
        });
    }
   
    function init() {
        loadCard();
        renderCard();
        buildEditorPanel();
        document.getElementById('applyIdentityBtn')?.addEventListener('click', () => {
            syncFromInputs();
            renderCard();
            alert(`Карточка привязана к нику: ${cardOwnerNick}`);
        });
    }
   
    init();
</script>
</body>
</html>[/html]

0

220

[html]<!DOCTYPE html>
<html>
<head>
    <meta charset="UTF-8">
    <title>Бункер | Карточка игрока</title>
    <style>
        * { box-sizing: border-box; }
        body {
            background: #1e2a1a;
            font-family: 'Segoe UI', 'Roboto', system-ui, sans-serif;
            display: flex;
            justify-content: center;
            align-items: center;
            padding: 30px 16px;
            margin: 0;
        }
        .bunker-container {
            max-width: 1100px;
            width: 100%;
        }
        .master-panel {
            background: #2f2b1fe0;
            backdrop-filter: blur(2px);
            border-radius: 48px;
            padding: 16px 24px;
            margin-bottom: 24px;
            display: flex;
            flex-wrap: wrap;
            gap: 16px;
            align-items: flex-end;
            justify-content: space-between;
        }
        .master-controls {
            display: flex;
            gap: 16px;
            flex-wrap: wrap;
            align-items: flex-end;
        }
        .master-controls div {
            display: flex;
            flex-direction: column;
            gap: 5px;
        }
        .master-controls label {
            color: #ffddb0;
            font-weight: bold;
            font-size: 0.7rem;
        }
        .master-controls input {
            background: #fff7ea;
            border: 1px solid #bf9a60;
            border-radius: 60px;
            padding: 8px 16px;
            font-family: monospace;
            width: 220px;
        }
        button {
            background: #598b3a;
            border: none;
            padding: 8px 20px;
            border-radius: 40px;
            font-weight: bold;
            color: #faf0cf;
            cursor: pointer;
            box-shadow: 0 3px 0 #2d4a1a;
            transition: 0.05s linear;
        }
        button:active {
            transform: translateY(2px);
            box-shadow: 0 1px 0 #2d4a1a;
        }
        .user-info {
            background: #1e2a1a;
            padding: 8px 18px;
            border-radius: 40px;
            color: #ffdd99;
            font-family: monospace;
        }
        .user-info span {
            color: #ffb347;
            font-weight: bold;
        }
        .player-card {
            background: #fdf2e0;
            border-radius: 40px;
            border: 2px solid #cb9a5e;
            box-shadow: 12px 12px 0 rgba(0,0,0,0.2);
            padding: 24px;
        }
        .identity-row {
            display: flex;
            gap: 24px;
            align-items: center;
            margin-bottom: 28px;
            flex-wrap: wrap;
            border-bottom: 2px dashed #dbb87a;
            padding-bottom: 20px;
        }
        .avatar {
            width: 85px;
            height: 85px;
            background: #7f5a38;
            border-radius: 24px;
            display: flex;
            align-items: center;
            justify-content: center;
            font-size: 3.2rem;
            box-shadow: inset 0 0 0 3px #fbe5b9, 0 6px 0 #33200c;
        }
        .nick-block {
            flex: 1;
        }
        .nick-label {
            font-size: 0.7rem;
            letter-spacing: 2px;
            color: #b47c44;
            font-weight: bold;
        }
        .player-nick {
            font-size: 1.8rem;
            font-weight: 800;
            font-family: monospace;
            background: #2a241a;
            display: inline-block;
            padding: 0 16px;
            border-radius: 60px;
            color: #ffda99;
        }
        .stats-grid {
            display: grid;
            grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
            gap: 16px;
        }
        .stat-card {
            background: #f9efdc;
            border-radius: 28px;
            border: 1px solid #e2c394;
            overflow: hidden;
        }
        .stat-header {
            background: #dbb067;
            padding: 8px 14px;
            font-weight: bold;
            font-size: 0.75rem;
            text-transform: uppercase;
            color: #2a2418;
            display: flex;
            justify-content: space-between;
        }
        .stat-value {
            padding: 16px 14px;
            min-height: 80px;
            background: #fff9ef;
            font-size: 0.95rem;
            word-break: break-word;
            cursor: pointer;
            transition: 0.05s;
            display: flex;
            align-items: center;
            justify-content: space-between;
        }
        .stat-value.closed {
            background: #e7d9c2;
            color: #6f5237;
            font-style: italic;
            justify-content: center;
        }
        .stat-value.opened {
            background: #fff2e2;
            border-left: 5px solid #719e3a;
            font-weight: 500;
            color: #1f2e0c;
        }
        .lock-icon {
            font-size: 0.7rem;
            background: #493a28;
            padding: 2px 8px;
            border-radius: 40px;
            color: #ffd89a;
        }
        .footer-info {
            margin-top: 24px;
            text-align: center;
            font-size: 0.7rem;
            background: #fff0db;
            border-radius: 30px;
            padding: 10px;
        }
        .editor-area {
            margin-top: 20px;
            background: #2e2a1c;
            border-radius: 32px;
            padding: 12px 18px;
        }
        .editor-toggle {
            cursor: pointer;
            color: #ffd89a;
            font-weight: bold;
            display: flex;
            justify-content: space-between;
        }
        .editor-content {
            display: none;
            margin-top: 15px;
        }
        textarea {
            width: 100%;
            background: #fff2e0;
            border-radius: 20px;
            border: 1px solid #b68b48;
            padding: 8px 12px;
            font-family: monospace;
        }
        @media (max-width: 650px) {
            .stats-grid { grid-template-columns: 1fr; }
            .player-nick { font-size: 1.3rem; }
        }
    </style>
</head>
<body>
<div class="bunker-container">
    <div class="master-panel">
        <div class="user-info" id="userInfoBlock">
            🔐 <span id="userStatusText">Определение вашего ID...</span>
        </div>
        <div class="master-controls">
            <div>
                <label>👤 НИК ВЛАДЕЛЬЦА КАРТОЧКИ</label>
                <input type="text" id="ownerNickInput" placeholder="Ник владельца" value="Mollymauk Tealeaf">
            </div>
            <div>
                <label>🖼️ Аватар (эмодзи)</label>
                <input type="text" id="avatarInput" value="🃏">
            </div>
            <button id="applyIdentityBtn">🎲 ПРИВЯЗАТЬ КАРТОЧКУ</button>
        </div>
    </div>

    <div id="cardRoot"></div>
    <div class="editor-area" id="editorPanel"></div>
   
    <div class="footer-info">
        🔐 <strong>Привязка по нику форума!</strong> Только игрок с ником <strong id="ownerNickDisplay">???</strong> может открывать характеристики.<br>
        Ваш текущий ник: <strong id="currentUserNickDisplay">???</strong> — если он совпадает с ником владельца, вы можете вскрывать данные.
    </div>
</div>

<script>
    // ==================== ОПРЕДЕЛЕНИЕ ПОЛЬЗОВАТЕЛЯ ИЗ ГЛОБАЛЬНЫХ ПЕРЕМЕННЫХ MYBB ====================
    let currentUserId = null;
    let currentUserNick = null;
   
    // Читаем переменные, которые форум уже объявил
    if (typeof UserID !== 'undefined' && UserID > 0) {
        currentUserId = UserID;
        console.log('[Бункер] UserID найден:', currentUserId);
    }
   
    if (typeof UserLogin !== 'undefined' && UserLogin) {
        currentUserNick = UserLogin;
        console.log('[Бункер] UserLogin найден:', currentUserNick);
    }
   
    // Если по каким-то причинам не определилось — пробуем альтернативные способы
    if (!currentUserNick) {
        // Пробуем через класс UserLogin
        const loginElem = document.querySelector('.UserLogin');
        if (loginElem) {
            currentUserNick = loginElem.textContent.trim();
            console.log('[Бункер] Ник через .UserLogin:', currentUserNick);
        }
    }
   
    // Если всё равно нет — запрашиваем вручную один раз
    if (!currentUserNick) {
        const savedNick = localStorage.getItem('bunker_user_nick');
        if (savedNick) {
            currentUserNick = savedNick;
        } else {
            let entered = prompt("🔐 Введите ваш никнейм на форуме (один раз):", "");
            if (entered && entered.trim()) {
                currentUserNick = entered.trim();
                localStorage.setItem('bunker_user_nick', currentUserNick);
            }
        }
    }
   
    // Отображение статуса
    function updateUserStatus() {
        const statusSpan = document.getElementById('userStatusText');
        const displaySpan = document.getElementById('currentUserNickDisplay');
        if (statusSpan) {
            if (currentUserNick) {
                statusSpan.innerHTML = `✅ Вы: ${currentUserNick} ${currentUserId ? '(ID: ' + currentUserId + ')' : ''}`;
            } else {
                statusSpan.innerHTML = `⚠️ Ник не определён. Обновите страницу.`;
            }
        }
        if (displaySpan) {
            displaySpan.textContent = currentUserNick || "не определён";
        }
    }
    updateUserStatus();
   
    // ==================== ДАННЫЕ КАРТОЧКИ ====================
    const STATS_LIST = [
        { id: "basic",     label: "🧬 Пол, возраст, плодовитость, стаж" },
        { id: "profession",label: "🔧 Профессия" },
        { id: "health",    label: "❤️ Здоровье" },
        { id: "bio",       label: "🧬 Биологические особенности" },
        { id: "hobby",     label: "🎨 Хобби / увлечение" },
        { id: "baggage",   label: "🎒 Багаж / Инвентарь" },
        { id: "phobia",    label: "😨 Фобии" },
        { id: "fact1",     label: "📖 Факт 1" },
        { id: "fact2",     label: "📖 Факт 2" }
    ];
   
    let cardOwnerNick = "Mollymauk Tealeaf";
    let cardAvatar = "🃏";
   
    let cardStats = {
        basic:      "Мужской, 28 лет, высокая плодовитость, стаж 5 лет",
        profession: "Карнавальный артист, жонглёр",
        health:     "Здоров, но есть шрамы",
        bio:        "Гибкий, ловкий, яркая внешность",
        hobby:      "Тарология, карточные фокусы",
        baggage:    "Колода карт, маска, бутылка виски",
        phobia:     "Боязнь тишины",
        fact1:      "Умеет читать мысли по картам",
        fact2:      "Никогда не снимает перчатки"
    };
   
    let revealedStats = {
        basic: false, profession: false, health: false, bio: false,
        hobby: false, baggage: false, phobia: false, fact1: false, fact2: false
    };
   
    function loadCard() {
        const saved = localStorage.getItem('bunker_card');
        if (saved) {
            try {
                const data = JSON.parse(saved);
                if (data.cardOwnerNick) cardOwnerNick = data.cardOwnerNick;
                if (data.cardAvatar) cardAvatar = data.cardAvatar;
                if (data.cardStats) cardStats = { ...cardStats, ...data.cardStats };
                if (data.revealedStats) revealedStats = { ...revealedStats, ...data.revealedStats };
            } catch(e) {}
        }
        const nickInput = document.getElementById('ownerNickInput');
        const avatarInput = document.getElementById('avatarInput');
        if (nickInput) nickInput.value = cardOwnerNick;
        if (avatarInput) avatarInput.value = cardAvatar;
        const nickDisplay = document.getElementById('ownerNickDisplay');
        if (nickDisplay) nickDisplay.textContent = cardOwnerNick;
    }
   
    function saveCard() {
        const data = { cardOwnerNick, cardAvatar, cardStats, revealedStats };
        localStorage.setItem('bunker_card', JSON.stringify(data));
    }
   
    function syncFromInputs() {
        const newNick = document.getElementById('ownerNickInput').value.trim();
        if (newNick) cardOwnerNick = newNick;
        const newAvatar = document.getElementById('avatarInput').value.trim();
        if (newAvatar) cardAvatar = newAvatar;
        saveCard();
        const nickDisplay = document.getElementById('ownerNickDisplay');
        if (nickDisplay) nickDisplay.textContent = cardOwnerNick;
    }
   
    function escapeHtml(str) {
        if (!str) return '';
        return str.replace(/[&<>]/g, function(m) {
            if (m === '&') return '&amp;';
            if (m === '<') return '&lt;';
            if (m === '>') return '&gt;';
            return m;
        });
    }
   
    function renderCard() {
        const container = document.getElementById('cardRoot');
        if (!container) return;
       
        let statsHtml = '';
        for (let stat of STATS_LIST) {
            const isOpen = revealedStats[stat.id];
            const statText = cardStats[stat.id] || "—";
            if (isOpen) {
                statsHtml += `
                    <div class="stat-card">
                        <div class="stat-header"><span>${stat.label}</span><span>🔓 открыто</span></div>
                        <div class="stat-value opened" data-stat-id="${stat.id}">✨ ${escapeHtml(statText)} ✨</div>
                    </div>
                `;
            } else {
                statsHtml += `
                    <div class="stat-card">
                        <div class="stat-header"><span>${stat.label}</span><span>🔒 закрыто</span></div>
                        <div class="stat-value closed" data-stat-id="${stat.id}">❓ НАЖМИТЕ, ЧТОБЫ ВСКРЫТЬ ❓<span class="lock-icon">закрыто</span></div>
                    </div>
                `;
            }
        }
       
        container.innerHTML = `
            <div class="player-card">
                <div class="identity-row">
                    <div class="avatar">${escapeHtml(cardAvatar)}</div>
                    <div class="nick-block">
                        <div class="nick-label">ВЛАДЕЛЕЦ КАРТОЧКИ</div>
                        <div class="player-nick">${escapeHtml(cardOwnerNick)}</div>
                    </div>
                </div>
                <div class="stats-grid" id="statsGridLive">${statsHtml}</div>
            </div>
        `;
        attachClickHandlers();
    }
   
    function attachClickHandlers() {
        document.querySelectorAll('.stat-value').forEach(div => {
            div.removeEventListener('click', statHandler);
            div.addEventListener('click', statHandler);
        });
    }
   
    function statHandler(e) {
        const target = e.currentTarget;
        const statId = target.getAttribute('data-stat-id');
        if (!statId) return;
       
        if (revealedStats[statId]) {
            alert(`🔓 Эта характеристика уже открыта!\n\n${cardStats[statId]}`);
            return;
        }
       
        // ПРОВЕРКА: текущий пользователь — владелец карточки?
        if (!currentUserNick) {
            let entered = prompt("🔐 Введите ваш никнейм на форуме:", "");
            if (entered && entered.trim()) {
                currentUserNick = entered.trim();
                localStorage.setItem('bunker_user_nick', currentUserNick);
                updateUserStatus();
            } else {
                alert("❌ Нужно ввести ник для проверки прав.");
                return;
            }
        }
       
        // Сравниваем ники (регистронезависимо для удобства)
        if (currentUserNick.toLowerCase() !== cardOwnerNick.toLowerCase()) {
            alert(`❌ ДОСТУП ЗАПРЕЩЁН!\nВы вошли как: "${currentUserNick}"\nЭта карточка принадлежит: "${cardOwnerNick}"\nОткрыть характеристики может только владелец карточки.`);
            return;
        }
       
        // Открываем
        revealedStats[statId] = true;
        saveCard();
        renderCard();
        alert(`✅ Игрок ${cardOwnerNick}, вы открыли характеристику:\n"${cardStats[statId]}"\nТеперь она видна всем.`);
    }
   
    function buildEditorPanel() {
        const panel = document.getElementById('editorPanel');
        if (!panel) return;
       
        panel.innerHTML = `
            <div class="editor-toggle" id="toggleEditorBtn">
                <strong>🛠️ РЕДАКТОР ХАРАКТЕРИСТИК (ведущий)</strong>
                <span>▼</span>
            </div>
            <div class="editor-content" id="editorContent">
                ${STATS_LIST.map(stat => `
                    <div style="margin-bottom: 12px;">
                        <label style="color:#ffcf87; display:block;">${stat.label}</label>
                        <textarea id="edit_${stat.id}" rows="2">${escapeHtml(cardStats[stat.id] || '')}</textarea>
                    </div>
                `).join('')}
                <div style="display: flex; gap: 12px; flex-wrap: wrap;">
                    <button id="saveStatsBtn">💾 СОХРАНИТЬ</button>
                    <button id="resetRevealsBtn">🔒 СБРОСИТЬ ОТКРЫТИЯ</button>
                    <button id="resetAllBtn" style="background:#8b5a3a;">🗑️ СБРОСИТЬ ВСЁ</button>
                </div>
            </div>
        `;
       
        const toggle = document.getElementById('toggleEditorBtn');
        const editorDiv = document.getElementById('editorContent');
        toggle.addEventListener('click', () => {
            if (editorDiv.style.display === 'none') {
                editorDiv.style.display = 'block';
                STATS_LIST.forEach(s => {
                    const ta = document.getElementById(`edit_${s.id}`);
                    if (ta) ta.value = cardStats[s.id] || '';
                });
            } else {
                editorDiv.style.display = 'none';
            }
        });
       
        document.getElementById('saveStatsBtn')?.addEventListener('click', () => {
            STATS_LIST.forEach(s => {
                const ta = document.getElementById(`edit_${s.id}`);
                if (ta) cardStats[s.id] = ta.value;
            });
            saveCard();
            renderCard();
            alert('✅ Характеристики обновлены');
        });
       
        document.getElementById('resetRevealsBtn')?.addEventListener('click', () => {
            if (confirm('Сбросить все открытия?')) {
                STATS_LIST.forEach(s => revealedStats[s.id] = false);
                saveCard();
                renderCard();
                alert('🔒 Все характеристики снова скрыты');
            }
        });
       
        document.getElementById('resetAllBtn')?.addEventListener('click', () => {
            if (confirm('ПОЛНЫЙ СБРОС карточки? Все данные вернутся к стандартным.')) {
                localStorage.removeItem('bunker_card');
                cardOwnerNick = document.getElementById('ownerNickInput')?.value.trim() || "Mollymauk Tealeaf";
                cardAvatar = "🃏";
                cardStats = {
                    basic: "Мужской, 28 лет, высокая плодовитость, стаж 5 лет",
                    profession: "Карнавальный артист, жонглёр",
                    health: "Здоров, но есть шрамы",
                    bio: "Гибкий, ловкий, яркая внешность",
                    hobby: "Тарология, карточные фокусы",
                    baggage: "Колода карт, маска, бутылка виски",
                    phobia: "Боязнь тишины",
                    fact1: "Умеет читать мысли по картам",
                    fact2: "Никогда не снимает перчатки"
                };
                STATS_LIST.forEach(s => revealedStats[s.id] = false);
                saveCard();
                document.getElementById('ownerNickInput').value = cardOwnerNick;
                document.getElementById('avatarInput').value = cardAvatar;
                renderCard();
                alert('💥 Карточка сброшена');
            }
        });
    }
   
    function init() {
        loadCard();
        renderCard();
        buildEditorPanel();
        document.getElementById('applyIdentityBtn')?.addEventListener('click', () => {
            syncFromInputs();
            renderCard();
            alert(`Карточка привязана к нику: ${cardOwnerNick}`);
        });
    }
   
    init();
</script>
</body>
</html>[/html]

0

221

[hideprofile][html]<div id="bunker-card" style="max-width:800px; margin:0 auto; font-family:system-ui; background:#f5f0e0; border-radius:20px; padding:20px; border:2px solid #cb9a5e;">
   
    <!-- Панель ведущего (настройка карточки) -->
    <div style="background:#2f2b1fe0; border-radius:16px; padding:12px; margin-bottom:20px;">
        <div style="display:flex; gap:12px; align-items:flex-end; flex-wrap:wrap;">
            <div>
                <div style="font-size:11px; color:#ffddb0;">👤 НИК ВЛАДЕЛЬЦА</div>
                <input type="text" id="ownerNickInput" style="padding:6px 12px; border-radius:20px; border:1px solid #bf9a60; background:#fff7ea;" placeholder="Введите ник игрока">
            </div>
            <div>
                <div style="font-size:11px; color:#ffddb0;">🖼️ АВАТАР (эмодзи)</div>
                <input type="text" id="avatarInput" style="padding:6px 12px; border-radius:20px; border:1px solid #bf9a60; background:#fff7ea;" placeholder="😀" value="🃏">
            </div>
            <button id="applyBtn" style="background:#598b3a; border:none; padding:6px 18px; border-radius:20px; color:white; cursor:pointer;">✅ ПРИВЯЗАТЬ</button>
        </div>
    </div>
   
    <!-- Карточка игрока -->
    <div style="display:flex; gap:20px; align-items:center; margin-bottom:20px; border-bottom:2px solid #dbb87a; padding-bottom:15px;">
        <div style="width:70px; height:70px; background:#7f5a38; border-radius:16px; display:flex; align-items:center; justify-content:center; font-size:2.5rem;" id="avatarDisplay">🃏</div>
        <div>
            <div style="font-size:0.7rem; color:#b47c44;">ВЛАДЕЛЕЦ КАРТОЧКИ</div>
            <div style="font-size:1.6rem; font-weight:bold; background:#2a241a; display:inline-block; padding:0 12px; border-radius:40px; color:#ffda99;" id="ownerNickDisplay">—</div>
        </div>
    </div>
   
    <div id="statsList" style="display:flex; flex-direction:column; gap:8px;"></div>
   
    <div style="margin-top:15px; padding:8px; background:#e8dcc8; border-radius:12px; font-size:12px; text-align:center;">
        🔐 Ваш ник: <strong id="currentUserDisplay">...</strong> — только вы можете открывать свои характеристики
    </div>
</div>

<script>
// ===== ОПРЕДЕЛЯЕМ ТЕКУЩЕГО ПОЛЬЗОВАТЕЛЯ =====
let currentUserId = typeof UserID !== 'undefined' ? UserID : null;
let currentUserNick = typeof UserLogin !== 'undefined' && UserLogin ? UserLogin : null;

if (!currentUserNick) {
    const loginElem = document.querySelector('.UserLogin');
    if (loginElem) currentUserNick = loginElem.textContent.trim();
}

if (!currentUserNick) {
    currentUserNick = localStorage.getItem('bunker_nick');
    if (!currentUserNick) {
        currentUserNick = prompt("Введите ваш ник на форуме:", "");
        if (currentUserNick) localStorage.setItem('bunker_nick', currentUserNick);
    }
}
document.getElementById('currentUserDisplay').innerText = currentUserNick || 'не определён';

// ===== ДАННЫЕ КАРТОЧКИ =====
let cardOwnerNick = localStorage.getItem('bunker_owner_nick') || "";
let cardAvatar = localStorage.getItem('bunker_avatar') || "🃏";

// Характеристики
const stats = [
    { name: "Пол, возраст, плодовитость, стаж", value: "Мужской, 28 лет, высокая, стаж 5 лет", opened: false },
    { name: "Профессия", value: "Карнавальный артист, жонглёр", opened: false },
    { name: "Здоровье", value: "Здоров, но есть шрамы", opened: false },
    { name: "Биологические особенности", value: "Гибкий, ловкий, яркая внешность", opened: false },
    { name: "Хобби / увлечение", value: "Тарология, карточные фокусы", opened: false },
    { name: "Багаж / Инвентарь", value: "Колода карт, маска, бутылка виски", opened: false },
    { name: "Фобии", value: "Боязнь тишины", opened: false },
    { name: "Факт 1", value: "Умеет читать мысли по картам", opened: false },
    { name: "Факт 2", value: "Никогда не снимает перчатки", opened: false }
];

// Загружаем открытые характеристики
const savedOpened = localStorage.getItem('bunker_opened');
if (savedOpened) {
    const openedArr = JSON.parse(savedOpened);
    stats.forEach((s, i) => { if (openedArr[i]) s.opened = true; });
}

// Загружаем сохранённого владельца
if (cardOwnerNick) {
    document.getElementById('ownerNickInput').value = cardOwnerNick;
    document.getElementById('ownerNickDisplay').innerText = cardOwnerNick;
    document.getElementById('avatarInput').value = cardAvatar;
    document.getElementById('avatarDisplay').innerText = cardAvatar;
}

function saveAll() {
    localStorage.setItem('bunker_owner_nick', cardOwnerNick);
    localStorage.setItem('bunker_avatar', cardAvatar);
    localStorage.setItem('bunker_opened', JSON.stringify(stats.map(s => s.opened)));
}

function renderStats() {
    const container = document.getElementById('statsList');
    container.innerHTML = stats.map((stat, idx) => `
        <div style="background:#fff9ef; border-radius:16px; border:1px solid #e2c394; overflow:hidden;">
            <div style="background:#dbb067; padding:6px 12px; font-weight:bold; font-size:0.75rem; display:flex; justify-content:space-between;">
                <span>${stat.name}</span>
                <span>${stat.opened ? '🔓 открыто' : '🔒 закрыто'}</span>
            </div>
            <div style="padding:12px; display:flex; justify-content:space-between; align-items:center;">
                <span style="${stat.opened ? '' : 'filter:blur(4px); background:#e7d9c2; padding:4px 8px; border-radius:12px;'}">
                    ${stat.opened ? stat.value : '❓ Скрытая информация'}
                </span>
                ${!stat.opened ? `<button class="reveal-btn" data-idx="${idx}" style="background:#598b3a; border:none; padding:4px 12px; border-radius:20px; color:white; cursor:pointer;">Открыть</button>` : ''}
            </div>
        </div>
    `).join('');
   
    document.querySelectorAll('.reveal-btn').forEach(btn => {
        btn.addEventListener('click', (e) => {
            const idx = parseInt(btn.dataset.idx);
           
            if (!currentUserNick) {
                alert("Не удалось определить ваш ник. Перезагрузите страницу.");
                return;
            }
           
            if (currentUserNick.toLowerCase() !== cardOwnerNick.toLowerCase()) {
                alert(`❌ Доступ запрещён!\nВы: ${currentUserNick}\nВладелец: ${cardOwnerNick}`);
                return;
            }
           
            if (!stats[idx].opened) {
                stats[idx].opened = true;
                saveAll();
                renderStats();
                alert(`✅ Вы открыли: ${stats[idx].name}\n\n${stats[idx].value}`);
            }
        });
    });
}

// Кнопка привязки карточки
document.getElementById('applyBtn').addEventListener('click', () => {
    const newNick = document.getElementById('ownerNickInput').value.trim();
    const newAvatar = document.getElementById('avatarInput').value.trim();
   
    if (!newNick) {
        alert("Введите ник владельца!");
        return;
    }
   
    cardOwnerNick = newNick;
    cardAvatar = newAvatar || "🃏";
   
    document.getElementById('ownerNickDisplay').innerText = cardOwnerNick;
    document.getElementById('avatarDisplay').innerText = cardAvatar;
   
    saveAll();
    alert(`✅ Карточка привязана к игроку: ${cardOwnerNick}`);
});

renderStats();
</script>[/html]

0

222

[hideprofile][html]<div id="bunker-card" style="max-width:800px; margin:0 auto; font-family:system-ui; background:#f5f0e0; border-radius:24px; padding:20px; border:2px solid #cb9a5e; box-shadow:0 4px 12px rgba(0,0,0,0.1);">
   
    <!-- Панель администратора (видна только модерам/админам) -->
    <div id="adminPanel" style="background:#2f2b1fe0; border-radius:16px; padding:12px; margin-bottom:20px; display:none;">
        <div style="font-size:12px; color:#ffddb0; margin-bottom:10px;">⚙️ УПРАВЛЕНИЕ КАРТОЧКОЙ (только для модераторов)</div>
        <div style="display:flex; gap:12px; flex-wrap:wrap; align-items:flex-end;">
            <div style="flex:1;">
                <div style="font-size:11px; color:#ffddb0;">👤 НИК ВЛАДЕЛЬЦА</div>
                <input type="text" id="ownerNickInput" style="width:100%; padding:8px 12px; border-radius:20px; border:none; background:#fff7ea;" placeholder="Введите ник игрока">
            </div>
            <div>
                <div style="font-size:11px; color:#ffddb0;">🖼️ АВАТАР (эмодзи)</div>
                <input type="text" id="avatarInput" style="width:80px; padding:8px 12px; border-radius:20px; border:none; background:#fff7ea;" value="🃏">
            </div>
            <button id="applyBtn" style="background:#598b3a; border:none; padding:8px 20px; border-radius:24px; color:white; cursor:pointer;">💾 СОХРАНИТЬ</button>
        </div>
        <hr style="margin:12px 0; border-color:#5a4a30;">
        <div style="display:flex; gap:12px; flex-wrap:wrap; align-items:flex-end;">
            <div style="flex:2;">
                <div style="font-size:11px; color:#ffddb0;">📦 ДОБАВИТЬ ВАРИАНТ В ПУЛ</div>
                <input type="text" id="newOptionInput" style="width:100%; padding:8px 12px; border-radius:20px; border:none; background:#fff7ea;" placeholder="Новый вариант характеристики">
            </div>
            <select id="statSelect" style="padding:8px 12px; border-radius:20px; border:none; background:#fff7ea;">
                <option value="basic">Пол, возраст, плодовитость, стаж</option>
                <option value="profession">Профессия</option>
                <option value="health">Здоровье</option>
                <option value="bio">Биологические особенности</option>
                <option value="hobby">Хобби / увлечение</option>
                <option value="baggage">Багаж / Инвентарь</option>
                <option value="phobia">Фобии</option>
                <option value="fact1">Факт 1</option>
                <option value="fact2">Факт 2</option>
            </select>
            <button id="addOptionBtn" style="background:#7f5a38; border:none; padding:8px 16px; border-radius:24px; color:white; cursor:pointer;">➕ ДОБАВИТЬ</button>
        </div>
    </div>
   
    <!-- Карточка игрока -->
    <div style="display:flex; gap:20px; align-items:center; margin-bottom:20px; border-bottom:2px solid #dbb87a; padding-bottom:16px;">
        <div style="width:75px; height:75px; background:#7f5a38; border-radius:20px; display:flex; align-items:center; justify-content:center; font-size:3rem; box-shadow:inset 0 0 0 3px #fbe5b9;" id="avatarDisplay">🃏</div>
        <div>
            <div style="font-size:0.7rem; color:#b47c44; letter-spacing:1px;">ВЛАДЕЛЕЦ КАРТОЧКИ</div>
            <div style="font-size:1.8rem; font-weight:bold; background:#2a241a; display:inline-block; padding:0 16px; border-radius:40px; color:#ffda99;" id="ownerNickDisplay">—</div>
        </div>
    </div>
   
    <!-- Список характеристик -->
    <div id="statsList" style="display:flex; flex-direction:column; gap:10px;"></div>
   
    <div style="margin-top:16px; padding:8px; background:#e8dcc8; border-radius:12px; font-size:12px; text-align:center; color:#5a4a30;">
        🔐 <strong id="roleCheckText">Проверка прав...</strong>
    </div>
</div>

<script>
// ========== ОПРЕДЕЛЕНИЕ ПРАВ АДМИНИСТРАТОРА ==========
// Группы с правами на открытие характеристик (обычно 1-4)
const adminGroups = [1, 2, 3, 4];
let currentGroupId = typeof GroupID !== 'undefined' ? GroupID : null;
let isAdmin = currentGroupId !== null && adminGroups.includes(currentGroupId);

// Определяем текущего пользователя
let currentUserNick = typeof UserLogin !== 'undefined' && UserLogin ? UserLogin : null;
if (!currentUserNick) {
    const loginElem = document.querySelector('.UserLogin');
    if (loginElem) currentUserNick = loginElem.textContent.trim();
}
if (!currentUserNick) {
    currentUserNick = localStorage.getItem('bunker_nick');
    if (!currentUserNick) {
        currentUserNick = prompt("Введите ваш ник на форуме:", "");
        if (currentUserNick) localStorage.setItem('bunker_nick', currentUserNick);
    }
}

// Показываем/скрываем админ-панель
if (isAdmin) {
    document.getElementById('adminPanel').style.display = 'block';
    document.getElementById('roleCheckText').innerHTML = `✅ Вы модератор/администратор — можете открывать характеристики и управлять пулом. Ваш ник: ${currentUserNick || '?'}`;
} else {
    document.getElementById('roleCheckText').innerHTML = `🔒 Вы: ${currentUserNick || 'гость'} — только администратор может открывать характеристики.`;
}

// ========== ПУЛЫ ХАРАКТЕРИСТИК ==========
// Загружаем сохранённые пулы или создаём стандартные
let pools = JSON.parse(localStorage.getItem('bunker_pools')) || {
    basic: [
        "Мужской, 28 лет, высокая плодовитость, стаж 5 лет",
        "Женский, 34 года, средняя плодовитость, стаж 12 лет",
        "Небинарный, 42 года, низкая плодовитость, стаж 20 лет",
        "Мужской, 19 лет, высокая плодовитость, стаж 0 лет",
        "Женский, 55 лет, отсутствует, стаж 30 лет"
    ],
    profession: [
        "Инженер-ремонтник, электрик",
        "Врач-хирург",
        "Военный, снайпер",
        "Повар, бармен",
        "Учитель истории",
        "Механик, водитель",
        "Программист, хакер"
    ],
    health: [
        "Здоров, лёгкая аллергия на пыльцу",
        "Хроническая астма, нужен ингалятор",
        "Здоров, крепкий иммунитет",
        "Проблемы со зрением, нужны очки",
        "Сердечная недостаточность",
        "Физически здоров, психологические травмы"
    ],
    bio: [
        "Ночное зрение, устойчивость к радиации",
        "Ускоренная регенерация",
        "Повышенная выносливость",
        "Сверхчувствительность к звукам",
        "Гибкость, ловкость",
        "Замедленный метаболизм"
    ],
    hobby: [
        "Шахматы и сборка дронов",
        "Игра на гитаре",
        "Коллекционирование марок",
        "Стрельба из лука",
        "Йога и медитация",
        "Кулинарные эксперименты"
    ],
    baggage: [
        "Рюкзак с инструментами, фонарик, НЗ консервов",
        "Аптечка, три банки тушёнки, нож",
        "Ноутбук с запасом батарей, пауэрбанк",
        "Колода карт, блокнот, ручка, фляга",
        "Спальный мешок, зажигалка, соль"
    ],
    phobia: [
        "Клаустрофобия (боязнь замкнутых пространств)",
        "Арахнофобия (боязнь пауков)",
        "Акрофобия (боязнь высоты)",
        "Некрофобия (боязнь трупов)",
        "Социофобия (боязнь людей)",
        "Никтофобия (боязнь темноты)"
    ],
    fact1: [
        "Был в армии, умеет стрелять",
        "Выиграл в лотерею 2 миллиона",
        "Бывший заключённый",
        "Имеет высшее образование МГУ",
        "Знает три иностранных языка",
        "Родился в бункере"
    ],
    fact2: [
        "Знает азы медицины",
        "Умеет вскрывать замки",
        "Был в эпицентре катастрофы",
        "Состоит в тайной организации",
        "Имеет двойное гражданство",
        "Родственник известного учёного"
    ]
};

// Состояние открытости характеристик
let revealed = JSON.parse(localStorage.getItem('bunker_revealed')) || {
    basic: false, profession: false, health: false, bio: false,
    hobby: false, baggage: false, phobia: false, fact1: false, fact2: false
};

let currentStats = JSON.parse(localStorage.getItem('bunker_stats')) || {
    basic: pools.basic[0],
    profession: pools.profession[0],
    health: pools.health[0],
    bio: pools.bio[0],
    hobby: pools.hobby[0],
    baggage: pools.baggage[0],
    phobia: pools.phobia[0],
    fact1: pools.fact1[0],
    fact2: pools.fact2[0]
};

let cardOwnerNick = localStorage.getItem('bunker_owner_nick') || "";
let cardAvatar = localStorage.getItem('bunker_avatar') || "🃏";

// ========== ФУНКЦИИ ==========
function randomFromPool(statKey) {
    const pool = pools[statKey];
    if (!pool || pool.length === 0) return "❌ Пул пуст, добавьте варианты";
    const randomIndex = Math.floor(Math.random() * pool.length);
    return pool[randomIndex];
}

function randomizeStat(statKey) {
    currentStats[statKey] = randomFromPool(statKey);
    saveToLocal();
    renderStats();
}

function addToPool(statKey, newValue) {
    if (!newValue || newValue.trim() === "") return false;
    if (!pools[statKey]) pools[statKey] = [];
    pools[statKey].push(newValue.trim());
    localStorage.setItem('bunker_pools', JSON.stringify(pools));
    return true;
}

function revealStat(statKey) {
    if (!isAdmin) {
        alert("❌ Только администратор/модератор может открывать характеристики!");
        return false;
    }
    revealed[statKey] = true;
    saveToLocal();
    renderStats();
    return true;
}

function saveToLocal() {
    localStorage.setItem('bunker_stats', JSON.stringify(currentStats));
    localStorage.setItem('bunker_revealed', JSON.stringify(revealed));
    localStorage.setItem('bunker_owner_nick', cardOwnerNick);
    localStorage.setItem('bunker_avatar', cardAvatar);
}

// Отображение ник и аватар
function updateIdentity() {
    document.getElementById('ownerNickDisplay').innerText = cardOwnerNick || "— не привязан —";
    document.getElementById('avatarDisplay').innerText = cardAvatar || "🃏";
    if (document.getElementById('ownerNickInput')) {
        document.getElementById('ownerNickInput').value = cardOwnerNick;
        document.getElementById('avatarInput').value = cardAvatar;
    }
}

// Рендер списка характеристик
function renderStats() {
    const container = document.getElementById('statsList');
    const statNames = {
        basic: "🧬 Пол, возраст, плодовитость, стаж",
        profession: "🔧 Профессия",
        health: "❤️ Здоровье",
        bio: "🧬 Биологические особенности",
        hobby: "🎨 Хобби / увлечение",
        baggage: "🎒 Багаж / Инвентарь",
        phobia: "😨 Фобии",
        fact1: "📖 Факт 1",
        fact2: "📖 Факт 2"
    };
   
    const statKeys = Object.keys(statNames);
   
    container.innerHTML = statKeys.map(key => `
        <div style="background:#fff9ef; border-radius:16px; border:1px solid #e2c394; overflow:hidden;">
            <div style="background:#dbb067; padding:8px 14px; font-weight:bold; font-size:0.75rem; display:flex; justify-content:space-between;">
                <span>${statNames[key]}</span>
                <span>${revealed[key] ? '🔓 ОТКРЫТО' : '🔒 ЗАКРЫТО'}</span>
            </div>
            <div style="padding:14px; background:${revealed[key] ? '#fff2e2' : '#e7d9c2'}">
                <div style="display:flex; justify-content:space-between; align-items:center; flex-wrap:wrap; gap:10px;">
                    <span style="flex:1; ${!revealed[key] ? 'filter:blur(4px);' : ''}">
                        ${revealed[key] ? (currentStats[key] || '❓ Не задано') : '❓ Скрытая информация'}
                    </span>
                    <div style="display:flex; gap:6px;">
                        ${isAdmin && !revealed[key] ? `
                            <button class="reveal-btn" data-key="${key}" style="background:#4a7a2e; border:none; padding:4px 12px; border-radius:20px; color:white; cursor:pointer;">🔓 ОТКРЫТЬ</button>
                        ` : ''}
                        ${isAdmin && revealed[key] ? `
                            <button class="randomize-btn" data-key="${key}" style="background:#7f5a38; border:none; padding:4px 12px; border-radius:20px; color:white; cursor:pointer;">🎲 РАНДОМ</button>
                        ` : ''}
                    </div>
                </div>
            </div>
        </div>
    `).join('');
   
    // Вешаем обработчики
    document.querySelectorAll('.reveal-btn').forEach(btn => {
        btn.addEventListener('click', (e) => {
            revealStat(btn.dataset.key);
        });
    });
   
    document.querySelectorAll('.randomize-btn').forEach(btn => {
        btn.addEventListener('click', (e) => {
            randomizeStat(btn.dataset.key);
        });
    });
}

// ========== ОБРАБОТЧИКИ КНОПОК ==========
document.getElementById('applyBtn')?.addEventListener('click', () => {
    const newNick = document.getElementById('ownerNickInput').value.trim();
    const newAvatar = document.getElementById('avatarInput').value.trim();
    if (newNick) cardOwnerNick = newNick;
    if (newAvatar) cardAvatar = newAvatar;
    updateIdentity();
    saveToLocal();
    alert(`✅ Карточка привязана к игроку: ${cardOwnerNick || 'ник не задан'}`);
});

document.getElementById('addOptionBtn')?.addEventListener('click', () => {
    const statKey = document.getElementById('statSelect').value;
    const newValue = document.getElementById('newOptionInput').value.trim();
    if (!newValue) {
        alert("Введите вариант характеристики!");
        return;
    }
    if (addToPool(statKey, newValue)) {
        document.getElementById('newOptionInput').value = '';
        alert(`✅ Добавлено в пул "${statKey}"`);
        // Не меняем текущую характеристику автоматически
    } else {
        alert("Ошибка добавления");
    }
});

// Инициализация
updateIdentity();
renderStats();
</script>
</div>[/html]

0

223

[html]<div id="bunker-card" style="max-width:800px; margin:0 auto; font-family:system-ui; background:#f5f0e0; border-radius:24px; padding:20px; border:2px solid #cb9a5e; box-shadow:0 4px 12px rgba(0,0,0,0.1);">
   
    <!-- Панель администратора (видна только модерам/админам) -->
    <div id="adminPanel" style="background:#2f2b1fe0; border-radius:16px; padding:12px; margin-bottom:20px; display:none;">
        <div style="font-size:12px; color:#ffddb0; margin-bottom:10px;">⚙️ УПРАВЛЕНИЕ КАРТОЧКОЙ (только для модераторов)</div>
        <div style="display:flex; gap:12px; flex-wrap:wrap; align-items:flex-end;">
            <div style="flex:1;">
                <div style="font-size:11px; color:#ffddb0;">👤 НИК ВЛАДЕЛЬЦА</div>
                <input type="text" id="ownerNickInput" style="width:100%; padding:8px 12px; border-radius:20px; border:none; background:#fff7ea;" placeholder="Введите ник игрока">
            </div>
            <div>
                <div style="font-size:11px; color:#ffddb0;">🖼️ АВАТАР (эмодзи)</div>
                <input type="text" id="avatarInput" style="width:80px; padding:8px 12px; border-radius:20px; border:none; background:#fff7ea;" value="🃏">
            </div>
            <button id="applyBtn" style="background:#598b3a; border:none; padding:8px 20px; border-radius:24px; color:white; cursor:pointer;">💾 СОХРАНИТЬ</button>
        </div>
        <hr style="margin:12px 0; border-color:#5a4a30;">
        <div style="display:flex; gap:12px; flex-wrap:wrap; align-items:flex-end;">
            <div style="flex:2;">
                <div style="font-size:11px; color:#ffddb0;">📦 ДОБАВИТЬ ВАРИАНТ В ПУЛ</div>
                <input type="text" id="newOptionInput" style="width:100%; padding:8px 12px; border-radius:20px; border:none; background:#fff7ea;" placeholder="Новый вариант характеристики">
            </div>
            <select id="statSelect" style="padding:8px 12px; border-radius:20px; border:none; background:#fff7ea;">
                <option value="basic">Пол, возраст, плодовитость, стаж</option>
                <option value="profession">Профессия</option>
                <option value="health">Здоровье</option>
                <option value="bio">Биологические особенности</option>
                <option value="hobby">Хобби / увлечение</option>
                <option value="baggage">Багаж / Инвентарь</option>
                <option value="phobia">Фобии</option>
                <option value="fact1">Факт 1</option>
                <option value="fact2">Факт 2</option>
            </select>
            <button id="addOptionBtn" style="background:#7f5a38; border:none; padding:8px 16px; border-radius:24px; color:white; cursor:pointer;">➕ ДОБАВИТЬ</button>
        </div>
    </div>
   
    <!-- Карточка игрока -->
    <div style="display:flex; gap:20px; align-items:center; margin-bottom:20px; border-bottom:2px solid #dbb87a; padding-bottom:16px;">
        <div style="width:75px; height:75px; background:#7f5a38; border-radius:20px; display:flex; align-items:center; justify-content:center; font-size:3rem; box-shadow:inset 0 0 0 3px #fbe5b9;" id="avatarDisplay">🃏</div>
        <div>
            <div style="font-size:0.7rem; color:#b47c44; letter-spacing:1px;">ВЛАДЕЛЕЦ КАРТОЧКИ</div>
            <div style="font-size:1.8rem; font-weight:bold; background:#2a241a; display:inline-block; padding:0 16px; border-radius:40px; color:#ffda99;" id="ownerNickDisplay">—</div>
        </div>
    </div>
   
    <!-- Список характеристик -->
    <div id="statsList" style="display:flex; flex-direction:column; gap:10px;"></div>
   
    <div style="margin-top:16px; padding:8px; background:#e8dcc8; border-radius:12px; font-size:12px; text-align:center; color:#5a4a30;">
        🔐 <strong id="roleCheckText">Проверка прав...</strong>
    </div>
</div>

<script>
// ========== ПОЛУЧАЕМ ДАННЫЕ ИЗ ГЛОБАЛЬНЫХ ПЕРЕМЕННЫХ ФОРУМА ==========
// Форум передаёт их в глобальную область, но внутри [html] они не видны.
// Поэтому мы создаём временный скрипт в глобальной области, который сохраняет их в data-атрибуты.

(function() {
    // Создаём скрипт, который выполнится в глобальной области
    var script = document.createElement('script');
    script.textContent = `
        window._bunkerGroupID = typeof GroupID !== 'undefined' ? GroupID : null;
        window._bunkerUserLogin = typeof UserLogin !== 'undefined' ? UserLogin : null;
        window._bunkerUserID = typeof UserID !== 'undefined' ? UserID : null;
        // Вешаем событие, чтобы скрипт карточки получил данные
        window.dispatchEvent(new CustomEvent('bunkerDataReady'));
    `;
    document.head.appendChild(script);
})();

// Ждём данные и запускаем основной код
window.addEventListener('bunkerDataReady', function() {
    startBunkerCard();
});

function startBunkerCard() {
    // Получаем данные из глобальных переменных
    let currentGroupId = window._bunkerGroupID;
    let currentUserNick = window._bunkerUserLogin;
    let currentUserId = window._bunkerUserID;
   
    // Группы с правами администратора (1 - администраторы, 2 - гл.модераторы, 3 - модераторы, 4 - помощники)
    const adminGroups = [1, 2, 3, 4];
    let isAdmin = currentGroupId !== null && adminGroups.includes(parseInt(currentGroupId));
   
    // Для отладки — выводим в консоль
    console.log('[Бункер] GroupID:', currentGroupId, 'isAdmin:', isAdmin, 'UserLogin:', currentUserNick);
   
    // Если не определился ник — пробуем через DOM
    if (!currentUserNick) {
        const loginElem = document.querySelector('.UserLogin');
        if (loginElem) currentUserNick = loginElem.textContent.trim();
    }
    if (!currentUserNick) {
        currentUserNick = localStorage.getItem('bunker_nick');
        if (!currentUserNick) {
            currentUserNick = prompt("Введите ваш ник на форуме:", "");
            if (currentUserNick) localStorage.setItem('bunker_nick', currentUserNick);
        }
    }
   
    // Показываем/скрываем админ-панель
    if (isAdmin) {
        document.getElementById('adminPanel').style.display = 'block';
        document.getElementById('roleCheckText').innerHTML = `✅ Вы администратор/модератор — можете открывать характеристики и управлять пулом. Ваш ник: ${currentUserNick || '?'}`;
    } else {
        document.getElementById('roleCheckText').innerHTML = `🔒 Вы: ${currentUserNick || 'гость'} — только администратор может открывать характеристики.`;
    }
   
    // ========== ПУЛЫ ХАРАКТЕРИСТИК ==========
    let pools = JSON.parse(localStorage.getItem('bunker_pools')) || {
        basic: ["Мужской, 28 лет, высокая плодовитость, стаж 5 лет", "Женский, 34 года, средняя плодовитость, стаж 12 лет", "Небинарный, 42 года, низкая плодовитость, стаж 20 лет", "Мужской, 19 лет, высокая плодовитость, стаж 0 лет", "Женский, 55 лет, отсутствует, стаж 30 лет"],
        profession: ["Инженер-ремонтник, электрик", "Врач-хирург", "Военный, снайпер", "Повар, бармен", "Учитель истории", "Механик, водитель", "Программист, хакер"],
        health: ["Здоров, лёгкая аллергия на пыльцу", "Хроническая астма, нужен ингалятор", "Здоров, крепкий иммунитет", "Проблемы со зрением, нужны очки", "Сердечная недостаточность", "Физически здоров, психологические травмы"],
        bio: ["Ночное зрение, устойчивость к радиации", "Ускоренная регенерация", "Повышенная выносливость", "Сверхчувствительность к звукам", "Гибкость, ловкость", "Замедленный метаболизм"],
        hobby: ["Шахматы и сборка дронов", "Игра на гитаре", "Коллекционирование марок", "Стрельба из лука", "Йога и медитация", "Кулинарные эксперименты"],
        baggage: ["Рюкзак с инструментами, фонарик, НЗ консервов", "Аптечка, три банки тушёнки, нож", "Ноутбук с запасом батарей, пауэрбанк", "Колода карт, блокнот, ручка, фляга", "Спальный мешок, зажигалка, соль"],
        phobia: ["Клаустрофобия (боязнь замкнутых пространств)", "Арахнофобия (боязнь пауков)", "Акрофобия (боязнь высоты)", "Некрофобия (боязнь трупов)", "Социофобия (боязнь людей)", "Никтофобия (боязнь темноты)"],
        fact1: ["Был в армии, умеет стрелять", "Выиграл в лотерею 2 миллиона", "Бывший заключённый", "Имеет высшее образование МГУ", "Знает три иностранных языка", "Родился в бункере"],
        fact2: ["Знает азы медицины", "Умеет вскрывать замки", "Был в эпицентре катастрофы", "Состоит в тайной организации", "Имеет двойное гражданство", "Родственник известного учёного"]
    };
   
    let revealed = JSON.parse(localStorage.getItem('bunker_revealed')) || {
        basic: false, profession: false, health: false, bio: false,
        hobby: false, baggage: false, phobia: false, fact1: false, fact2: false
    };
   
    let currentStats = JSON.parse(localStorage.getItem('bunker_stats')) || {
        basic: pools.basic[0], profession: pools.profession[0], health: pools.health[0],
        bio: pools.bio[0], hobby: pools.hobby[0], baggage: pools.baggage[0],
        phobia: pools.phobia[0], fact1: pools.fact1[0], fact2: pools.fact2[0]
    };
   
    let cardOwnerNick = localStorage.getItem('bunker_owner_nick') || "";
    let cardAvatar = localStorage.getItem('bunker_avatar') || "🃏";
   
    function randomFromPool(statKey) {
        const pool = pools[statKey];
        if (!pool || pool.length === 0) return "❌ Пул пуст, добавьте варианты";
        return pool[Math.floor(Math.random() * pool.length)];
    }
   
    function randomizeStat(statKey) {
        currentStats[statKey] = randomFromPool(statKey);
        saveToLocal();
        renderStats();
    }
   
    function addToPool(statKey, newValue) {
        if (!newValue || newValue.trim() === "") return false;
        if (!pools[statKey]) pools[statKey] = [];
        pools[statKey].push(newValue.trim());
        localStorage.setItem('bunker_pools', JSON.stringify(pools));
        return true;
    }
   
    function revealStat(statKey) {
        if (!isAdmin) {
            alert("❌ Только администратор/модератор может открывать характеристики!");
            return false;
        }
        revealed[statKey] = true;
        saveToLocal();
        renderStats();
        return true;
    }
   
    function saveToLocal() {
        localStorage.setItem('bunker_stats', JSON.stringify(currentStats));
        localStorage.setItem('bunker_revealed', JSON.stringify(revealed));
        localStorage.setItem('bunker_owner_nick', cardOwnerNick);
        localStorage.setItem('bunker_avatar', cardAvatar);
    }
   
    function updateIdentity() {
        document.getElementById('ownerNickDisplay').innerText = cardOwnerNick || "— не привязан —";
        document.getElementById('avatarDisplay').innerText = cardAvatar || "🃏";
        if (document.getElementById('ownerNickInput')) {
            document.getElementById('ownerNickInput').value = cardOwnerNick;
            document.getElementById('avatarInput').value = cardAvatar;
        }
    }
   
    function renderStats() {
        const container = document.getElementById('statsList');
        const statNames = {
            basic: "🧬 Пол, возраст, плодовитость, стаж",
            profession: "🔧 Профессия",
            health: "❤️ Здоровье",
            bio: "🧬 Биологические особенности",
            hobby: "🎨 Хобби / увлечение",
            baggage: "🎒 Багаж / Инвентарь",
            phobia: "😨 Фобии",
            fact1: "📖 Факт 1",
            fact2: "📖 Факт 2"
        };
        const statKeys = Object.keys(statNames);
       
        container.innerHTML = statKeys.map(key => `
            <div style="background:#fff9ef; border-radius:16px; border:1px solid #e2c394; overflow:hidden;">
                <div style="background:#dbb067; padding:8px 14px; font-weight:bold; font-size:0.75rem; display:flex; justify-content:space-between;">
                    <span>${statNames[key]}</span>
                    <span>${revealed[key] ? '🔓 ОТКРЫТО' : '🔒 ЗАКРЫТО'}</span>
                </div>
                <div style="padding:14px; background:${revealed[key] ? '#fff2e2' : '#e7d9c2'}">
                    <div style="display:flex; justify-content:space-between; align-items:center; flex-wrap:wrap; gap:10px;">
                        <span style="flex:1; ${!revealed[key] ? 'filter:blur(4px);' : ''}">
                            ${revealed[key] ? (currentStats[key] || '❓ Не задано') : '❓ Скрытая информация'}
                        </span>
                        <div style="display:flex; gap:6px;">
                            ${isAdmin && !revealed[key] ? `<button class="reveal-btn" data-key="${key}" style="background:#4a7a2e; border:none; padding:4px 12px; border-radius:20px; color:white; cursor:pointer;">🔓 ОТКРЫТЬ</button>` : ''}
                            ${isAdmin && revealed[key] ? `<button class="randomize-btn" data-key="${key}" style="background:#7f5a38; border:none; padding:4px 12px; border-radius:20px; color:white; cursor:pointer;">🎲 РАНДОМ</button>` : ''}
                        </div>
                    </div>
                </div>
            </div>
        `).join('');
       
        document.querySelectorAll('.reveal-btn').forEach(btn => {
            btn.addEventListener('click', () => revealStat(btn.dataset.key));
        });
        document.querySelectorAll('.randomize-btn').forEach(btn => {
            btn.addEventListener('click', () => randomizeStat(btn.dataset.key));
        });
    }
   
    document.getElementById('applyBtn')?.addEventListener('click', () => {
        const newNick = document.getElementById('ownerNickInput').value.trim();
        const newAvatar = document.getElementById('avatarInput').value.trim();
        if (newNick) cardOwnerNick = newNick;
        if (newAvatar) cardAvatar = newAvatar;
        updateIdentity();
        saveToLocal();
        alert(`✅ Карточка привязана к игроку: ${cardOwnerNick || 'ник не задан'}`);
    });
   
    document.getElementById('addOptionBtn')?.addEventListener('click', () => {
        const statKey = document.getElementById('statSelect').value;
        const newValue = document.getElementById('newOptionInput').value.trim();
        if (!newValue) {
            alert("Введите вариант характеристики!");
            return;
        }
        if (addToPool(statKey, newValue)) {
            document.getElementById('newOptionInput').value = '';
            alert(`✅ Добавлено в пул "${statKey}"`);
        } else {
            alert("Ошибка добавления");
        }
    });
   
    updateIdentity();
    renderStats();
}
</script>[/html]

0

224

[html]<div id="bunker-card" style="max-width:800px; margin:0 auto; font-family:system-ui; background:#f5f0e0; border-radius:24px; padding:20px; border:2px solid #cb9a5e;">
   
    <!-- Панель администратора -->
    <div id="adminPanel" style="background:#2f2b1fe0; border-radius:16px; padding:12px; margin-bottom:20px; display:none;">
        <div style="font-size:12px; color:#ffddb0; margin-bottom:10px;">⚙️ УПРАВЛЕНИЕ КАРТОЧКОЙ (только для модераторов)</div>
        <div style="display:flex; gap:12px; flex-wrap:wrap; align-items:flex-end;">
            <div style="flex:1;">
                <div style="font-size:11px; color:#ffddb0;">👤 НИК ВЛАДЕЛЬЦА</div>
                <input type="text" id="ownerNickInput" style="width:100%; padding:8px 12px; border-radius:20px; border:none; background:#fff7ea;" placeholder="Введите ник игрока">
            </div>
            <div>
                <div style="font-size:11px; color:#ffddb0;">🖼️ АВАТАР (эмодзи)</div>
                <input type="text" id="avatarInput" style="width:80px; padding:8px 12px; border-radius:20px; border:none; background:#fff7ea;" value="🃏">
            </div>
            <button id="applyBtn" style="background:#598b3a; border:none; padding:8px 20px; border-radius:24px; color:white; cursor:pointer;">💾 СОХРАНИТЬ</button>
        </div>
        <hr style="margin:12px 0; border-color:#5a4a30;">
        <div style="display:flex; gap:12px; flex-wrap:wrap; align-items:flex-end;">
            <div style="flex:2;">
                <div style="font-size:11px; color:#ffddb0;">📦 ДОБАВИТЬ ВАРИАНТ В ПУЛ</div>
                <input type="text" id="newOptionInput" style="width:100%; padding:8px 12px; border-radius:20px; border:none; background:#fff7ea;" placeholder="Новый вариант характеристики">
            </div>
            <select id="statSelect" style="padding:8px 12px; border-radius:20px; border:none; background:#fff7ea;">
                <option value="basic">Пол, возраст, плодовитость, стаж</option>
                <option value="profession">Профессия</option>
                <option value="health">Здоровье</option>
                <option value="bio">Биологические особенности</option>
                <option value="hobby">Хобби / увлечение</option>
                <option value="baggage">Багаж / Инвентарь</option>
                <option value="phobia">Фобии</option>
                <option value="fact1">Факт 1</option>
                <option value="fact2">Факт 2</option>
            </select>
            <button id="addOptionBtn" style="background:#7f5a38; border:none; padding:8px 16px; border-radius:24px; color:white; cursor:pointer;">➕ ДОБАВИТЬ</button>
        </div>
    </div>
   
    <!-- Карточка игрока -->
    <div style="display:flex; gap:20px; align-items:center; margin-bottom:20px; border-bottom:2px solid #dbb87a; padding-bottom:16px;">
        <div style="width:75px; height:75px; background:#7f5a38; border-radius:20px; display:flex; align-items:center; justify-content:center; font-size:3rem; box-shadow:inset 0 0 0 3px #fbe5b9;" id="avatarDisplay">🃏</div>
        <div>
            <div style="font-size:0.7rem; color:#b47c44;">ВЛАДЕЛЕЦ КАРТОЧКИ</div>
            <div style="font-size:1.8rem; font-weight:bold; background:#2a241a; display:inline-block; padding:0 16px; border-radius:40px; color:#ffda99;" id="ownerNickDisplay">—</div>
        </div>
    </div>
   
    <div id="statsList" style="display:flex; flex-direction:column; gap:10px;"></div>
   
    <div style="margin-top:16px; padding:8px; background:#e8dcc8; border-radius:12px; font-size:12px; text-align:center;">
        🔐 <strong id="roleCheckText">Проверка прав...</strong>
    </div>
</div>

<script>
// ===== ПРОСТОЙ СПОСОБ: читаем переменные прямо из HTML =====
var bunkerGroupID = null;
var bunkerUserLogin = null;

// Ищем скрипт с переменными на странице
var scripts = document.getElementsByTagName('script');
for (var i = 0; i < scripts.length; i++) {
    var scriptText = scripts[i].innerHTML || scripts[i].textContent;
    if (scriptText && scriptText.indexOf('var GroupID') !== -1) {
        // Нашли скрипт с переменными
        var matchGroup = scriptText.match(/var GroupID = (\d+);/);
        var matchLogin = scriptText.match(/var UserLogin = '([^']+)';/);
        if (matchGroup) bunkerGroupID = parseInt(matchGroup[1]);
        if (matchLogin) bunkerUserLogin = matchLogin[1];
        break;
    }
}

// Группы с правами администратора (1 - админ, 2 - гл.мод, 3 - мод, 4 - пом.мод)
var adminGroups = [1, 2, 3, 4];
var isAdmin = bunkerGroupID !== null && adminGroups.indexOf(bunkerGroupID) !== -1;
var currentUserNick = bunkerUserLogin;

// Отладка в консоли
console.log('Бункер: GroupID =', bunkerGroupID, 'isAdmin =', isAdmin, 'UserLogin =', currentUserNick);

// Если ник не определился
if (!currentUserNick) {
    currentUserNick = localStorage.getItem('bunker_nick');
    if (!currentUserNick) {
        currentUserNick = prompt("Введите ваш ник на форуме:", "");
        if (currentUserNick) localStorage.setItem('bunker_nick', currentUserNick);
    }
}

// Показываем админ-панель если админ
if (isAdmin) {
    document.getElementById('adminPanel').style.display = 'block';
    document.getElementById('roleCheckText').innerHTML = '✅ Вы администратор/модератор — можете открывать характеристики. Ваш ник: ' + (currentUserNick || '?');
} else {
    document.getElementById('roleCheckText').innerHTML = '🔒 Вы: ' + (currentUserNick || 'гость') + ' — только администратор может открывать характеристики.';
}

// ===== ДАННЫЕ КАРТОЧКИ =====
var pools = JSON.parse(localStorage.getItem('bunker_pools')) || {
    basic: ["Мужской, 28 лет, высокая плодовитость, стаж 5 лет", "Женский, 34 года, средняя плодовитость, стаж 12 лет"],
    profession: ["Инженер-ремонтник", "Врач-хирург", "Военный, снайпер"],
    health: ["Здоров, лёгкая аллергия", "Хроническая астма", "Здоров, крепкий иммунитет"],
    bio: ["Ночное зрение", "Ускоренная регенерация", "Повышенная выносливость"],
    hobby: ["Шахматы", "Игра на гитаре", "Коллекционирование"],
    baggage: ["Рюкзак с инструментами", "Аптечка, тушёнка", "Ноутбук, пауэрбанк"],
    phobia: ["Клаустрофобия", "Арахнофобия", "Акрофобия"],
    fact1: ["Был в армии", "Выиграл в лотерею", "Бывший заключённый"],
    fact2: ["Знает медицину", "Умеет вскрывать замки", "Был в эпицентре катастрофы"]
};

var revealed = JSON.parse(localStorage.getItem('bunker_revealed')) || {};
var currentStats = JSON.parse(localStorage.getItem('bunker_stats')) || {};
var cardOwnerNick = localStorage.getItem('bunker_owner_nick') || "";
var cardAvatar = localStorage.getItem('bunker_avatar') || "🃏";

// Инициализация пустых значений
var statKeys = ['basic', 'profession', 'health', 'bio', 'hobby', 'baggage', 'phobia', 'fact1', 'fact2'];
for (var i = 0; i < statKeys.length; i++) {
    var key = statKeys[i];
    if (!revealed[key]) revealed[key] = false;
    if (!currentStats[key]) currentStats[key] = pools[key][0];
}

function saveAll() {
    localStorage.setItem('bunker_stats', JSON.stringify(currentStats));
    localStorage.setItem('bunker_revealed', JSON.stringify(revealed));
    localStorage.setItem('bunker_owner_nick', cardOwnerNick);
    localStorage.setItem('bunker_avatar', cardAvatar);
}

function randomFromPool(key) {
    var pool = pools[key];
    return pool[Math.floor(Math.random() * pool.length)];
}

function randomizeStat(key) {
    currentStats[key] = randomFromPool(key);
    saveAll();
    renderStats();
}

function revealStat(key) {
    if (!isAdmin) {
        alert("❌ Только администратор может открывать характеристики!");
        return;
    }
    revealed[key] = true;
    saveAll();
    renderStats();
}

function addToPool(key, value) {
    if (!value || value.trim() === "") return false;
    if (!pools[key]) pools[key] = [];
    pools[key].push(value.trim());
    localStorage.setItem('bunker_pools', JSON.stringify(pools));
    return true;
}

function updateIdentity() {
    document.getElementById('ownerNickDisplay').innerText = cardOwnerNick || "— не привязан —";
    document.getElementById('avatarDisplay').innerText = cardAvatar || "🃏";
    if (document.getElementById('ownerNickInput')) {
        document.getElementById('ownerNickInput').value = cardOwnerNick;
        document.getElementById('avatarInput').value = cardAvatar;
    }
}

function renderStats() {
    var statNames = {
        basic: "🧬 Пол, возраст, плодовитость, стаж",
        profession: "🔧 Профессия",
        health: "❤️ Здоровье",
        bio: "🧬 Биологические особенности",
        hobby: "🎨 Хобби / увлечение",
        baggage: "🎒 Багаж / Инвентарь",
        phobia: "😨 Фобии",
        fact1: "📖 Факт 1",
        fact2: "📖 Факт 2"
    };
    var keys = Object.keys(statNames);
    var html = '';
   
    for (var i = 0; i < keys.length; i++) {
        var key = keys[i];
        var isOpen = revealed[key];
        var value = currentStats[key] || '❓ Не задано';
       
        html += '<div style="background:#fff9ef; border-radius:16px; border:1px solid #e2c394; margin-bottom:10px;">';
        html += '<div style="background:#dbb067; padding:8px 14px; font-weight:bold; font-size:0.75rem; display:flex; justify-content:space-between;">';
        html += '<span>' + statNames[key] + '</span>';
        html += '<span>' + (isOpen ? '🔓 ОТКРЫТО' : '🔒 ЗАКРЫТО') + '</span>';
        html += '</div>';
        html += '<div style="padding:14px; background:' + (isOpen ? '#fff2e2' : '#e7d9c2') + ';">';
        html += '<div style="display:flex; justify-content:space-between; align-items:center; flex-wrap:wrap; gap:10px;">';
        html += '<span style="flex:1;' + (!isOpen ? 'filter:blur(4px);' : '') + '">' + (isOpen ? value : '❓ Скрытая информация') + '</span>';
        html += '<div style="display:flex; gap:6px;">';
        if (isAdmin && !isOpen) html += '<button class="reveal-btn" data-key="' + key + '" style="background:#4a7a2e; border:none; padding:4px 12px; border-radius:20px; color:white; cursor:pointer;">🔓 ОТКРЫТЬ</button>';
        if (isAdmin && isOpen) html += '<button class="randomize-btn" data-key="' + key + '" style="background:#7f5a38; border:none; padding:4px 12px; border-radius:20px; color:white; cursor:pointer;">🎲 РАНДОМ</button>';
        html += '</div></div></div></div>';
    }
   
    document.getElementById('statsList').innerHTML = html;
   
    // Обработчики
    var revealBtns = document.querySelectorAll('.reveal-btn');
    for (var i = 0; i < revealBtns.length; i++) {
        revealBtns[i].addEventListener('click', function(e) {
            revealStat(this.dataset.key);
        });
    }
   
    var randomBtns = document.querySelectorAll('.randomize-btn');
    for (var i = 0; i < randomBtns.length; i++) {
        randomBtns[i].addEventListener('click', function(e) {
            randomizeStat(this.dataset.key);
        });
    }
}

// Кнопки
document.getElementById('applyBtn').addEventListener('click', function() {
    var newNick = document.getElementById('ownerNickInput').value.trim();
    var newAvatar = document.getElementById('avatarInput').value.trim();
    if (newNick) cardOwnerNick = newNick;
    if (newAvatar) cardAvatar = newAvatar;
    updateIdentity();
    saveAll();
    alert('✅ Карточка привязана к: ' + (cardOwnerNick || 'ник не задан'));
});

document.getElementById('addOptionBtn').addEventListener('click', function() {
    var statKey = document.getElementById('statSelect').value;
    var newValue = document.getElementById('newOptionInput').value.trim();
    if (!newValue) {
        alert("Введите вариант характеристики!");
        return;
    }
    if (addToPool(statKey, newValue)) {
        document.getElementById('newOptionInput').value = '';
        alert('✅ Добавлено в пул');
    } else {
        alert("Ошибка");
    }
});

// Запуск
updateIdentity();
renderStats();
</script>
</div>[/html]

0

225

[html]<!DOCTYPE html>
<html lang="ru">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Бункер: Карточка игрока (для MyBB)</title>
    <style>
        /* Самый простой, нейтральный визуал — без лишних украшений */
        body {
            background: #f0f2f5;
            font-family: system-ui, -apple-system, 'Segoe UI', Roboto, 'Helvetica Neue', sans-serif;
            margin: 0;
            padding: 20px;
        }
        .container {
            max-width: 800px;
            margin: 0 auto;
        }
        /* Блок управления — только для автора (скрыт визуально от других, но важен сам мех) */
        .author-tools {
            background: #e9ecef;
            padding: 12px 16px;
            border-radius: 16px;
            margin-bottom: 24px;
            font-size: 14px;
            border-left: 4px solid #2c7da0;
        }
        .author-tools label {
            font-weight: 600;
            margin-right: 8px;
        }
        .author-tools input, .author-tools textarea {
            font-family: monospace;
            font-size: 13px;
            padding: 6px 8px;
            border: 1px solid #adb5bd;
            border-radius: 10px;
            background: white;
        }
        .author-tools textarea {
            width: 94%;
            height: 70px;
            margin-top: 6px;
        }
        .flex-btns {
            display: flex;
            flex-wrap: wrap;
            gap: 12px;
            margin-top: 12px;
            align-items: center;
        }
        button {
            background: #2c7da0;
            border: none;
            color: white;
            padding: 6px 14px;
            border-radius: 40px;
            font-size: 13px;
            font-weight: 500;
            cursor: pointer;
            transition: 0.1s;
        }
        button:hover {
            background: #1f5e7a;
        }
        .roll-btn {
            background: #5f6c7a;
        }
        .roll-btn:hover {
            background: #3a4653;
        }
        /* Карточка игрока — простая сетка */
        .player-card {
            background: white;
            border-radius: 24px;
            padding: 20px;
            box-shadow: 0 4px 12px rgba(0,0,0,0.08);
            margin-top: 10px;
        }
        .row-avatar-nick {
            display: flex;
            gap: 20px;
            align-items: center;
            margin-bottom: 24px;
            flex-wrap: wrap;
        }
        .avatar-box {
            width: 96px;
            height: 96px;
            background: #dee2e6;
            border-radius: 20px;
            display: flex;
            align-items: center;
            justify-content: center;
            overflow: hidden;
            font-size: 12px;
            color: #6c757d;
            flex-shrink: 0;
            border: 1px solid #ced4da;
        }
        .avatar-box img {
            width: 100%;
            height: 100%;
            object-fit: cover;
        }
        .nickname {
            font-size: 28px;
            font-weight: 700;
            color: #1e2a3e;
            word-break: break-word;
        }
        /* Сетка характеристик */
        .stats-grid {
            display: flex;
            flex-direction: column;
            gap: 12px;
        }
        .stat-item {
            background: #f8f9fa;
            border-radius: 18px;
            padding: 10px 16px;
            border-left: 5px solid #adb5bd;
            font-size: 14px;
            transition: background 0.1s;
        }
        .stat-label {
            font-weight: 700;
            display: inline-block;
            width: 150px;
            color: #2c3e50;
        }
        .stat-value {
            font-weight: 500;
            color: #0b1c2c;
            word-break: break-word;
        }
        .closed {
            filter: blur(4px);
            background: #e9ecef;
            color: transparent;
            user-select: none;
            cursor: pointer;
            display: inline-block;
            padding: 2px 10px;
            border-radius: 20px;
            background: #dee2e6;
            font-style: italic;
        }
        .closed:hover {
            filter: blur(3px);
        }
        .open-badge {
            font-size: 11px;
            background: #cfe2ff;
            border-radius: 20px;
            padding: 2px 8px;
            margin-left: 10px;
            font-weight: normal;
            color: #004085;
        }
        .stat-control {
            margin-top: 8px;
            font-size: 12px;
        }
        .small-btn {
            background: none;
            border: 1px solid #adb5bd;
            color: #2c3e50;
            padding: 2px 12px;
            font-size: 11px;
            border-radius: 30px;
            cursor: pointer;
            background: white;
        }
        .small-btn:hover {
            background: #e2e6ea;
        }
        hr {
            margin: 16px 0;
            border-top: 1px solid #e2e8f0;
        }
        .footer-note {
            font-size: 12px;
            color: #6c757d;
            text-align: center;
            margin-top: 20px;
        }
    </style>
</head>
<body>
<div class="container">
    <!-- Панель управления — видна всем, но реально данные может менять только "автор сообщения" (тому, у кого есть пароль/ключ)
         Поскольку в MyBB нет встроенного разделения через скрипт, добавим простой "режим автора" по кнопке/паролю.
         Для удобства: нажимаем "Режим автора (редактирование)" и вводим пароль (по умолчанию bunker2024).
         Либо автор может править напрямую — в реальном форуме только автор сообщения видит кнопку редактирования, но этот скрипт эмулирует.
         Сделаем: редактировать все поля и ролл могут только после ввода ПАРОЛЯ (авторский доступ). Для простоты — одиночный пароль. -->
    <div class="author-tools">
        <div><strong>🔐 Зона автора сообщения</strong> (только создатель карточек)</div>
        <div style="display: flex; gap: 12px; align-items: center; margin-top: 8px;">
            <input type="password" id="authorPass" placeholder="Пароль редактора" style="width: 160px;">
            <button id="unlockBtn">🔓 Разблокировать редактирование</button>
            <span id="lockStatus" style="font-size:13px; color:#a00;">🔒 Закрыто</span>
        </div>
        <div id="authorPanel" style="display: none; margin-top: 16px;">
            <div><strong>✏️ Редактирование карточки:</strong></div>
            <div style="display: flex; flex-wrap: wrap; gap: 12px; margin: 10px 0;">
                <input type="text" id="editNick" placeholder="Ник игрока" style="width: 180px;">
                <input type="text" id="editAvatarUrl" placeholder="URL аватара (квадратное)" style="width: 220px;">
                <button id="updateBasicBtn">Обновить ник/аватар</button>
            </div>
            <div><strong>🎲 Ролл характеристик из пула:</strong></div>
            <div style="margin: 8px 0;">
                <textarea id="poolTextarea" rows="2" placeholder="Пул характеристик (каждая с новой строки) Например: Мужчина, 34 года, высокая плодовитость, стаж 12 лет Водитель Здоров: крепкое сердце Аллергия на пыльцу Коллекционирование марок Рюкзак с консервами Акрофобия Выиграл в лотерею Умеет готовить"></textarea>
                <div class="flex-btns">
                    <button id="rollStatBtn" class="roll-btn">🎲 Случайная характеристика (выбрать поле и заменить)</button>
                    <select id="statToRoll">
                        <option value="base">Пол, возраст, плодовитость, стаж</option>
                        <option value="profession">Профессия</option>
                        <option value="health">Здоровье</option>
                        <option value="bio">Биологические особенности</option>
                        <option value="hobby">Хобби</option>
                        <option value="baggage">Багаж / Инвентарь</option>
                        <option value="phobia">Фобии</option>
                        <option value="fact1">Факт 1</option>
                        <option value="fact2">Факт 2</option>
                    </select>
                    <button id="addToPoolBtn">➕ Добавить строку в пул</button>
                </div>
            </div>
            <div><small>⚡ Чтобы закрыть/открыть любую характеристику — нажмите на неё (только в режиме автора доступно открытие для всех?). Правило: открыть характеристику может только автор. Нажмите на "закрыто" чтобы открыть (автор). Для удобства автор также может снять блок.</small></div>
        </div>
    </div>

    <!-- КАРТОЧКА ИГРОКА -->
    <div class="player-card" id="playerCard">
        <div class="row-avatar-nick">
            <div class="avatar-box" id="avatarBox">
                <div id="avatarPreview" style="width:100%;height:100%;display:flex;align-items:center;justify-content:center;background:#e2e8f0;color:#4b5563;">🖼️</div>
            </div>
            <div class="nickname" id="nicknameDisplay">Игрок</div>
        </div>
        <div class="stats-grid" id="statsGrid"></div>
    </div>
    <div class="footer-note">✔️ Карточка инициализирована. Только автор сообщения может менять данные и открывать/роллить характеристики.</div>
</div>

<script>
    // ------------------- Хранилище состояний ------------------
    // Характеристики: значения + флаг открыто/закрыто (изначально все закрыты)
    let charData = {
        base: { value: "Мужчина, 28 лет, норма, стаж 5 лет", open: false },     // пол/возраст/плод/стаж
        profession: { value: "Инженер", open: false },
        health: { value: "Отличное", open: false },
        bio: { value: "Нет особенностей", open: false },
        hobby: { value: "Шахматы", open: false },
        baggage: { value: "Фонарик и спички", open: false },
        phobia: { value: "Клаустрофобия", open: false },
        fact1: { value: "Был в армии", open: false },
        fact2: { value: "Любит собак", open: false }
    };
   
    let currentNick = "Выживший";
    let currentAvatarUrl = "";
   
    // Пул характеристик (пример)
    let characteristicsPool = [
        "Мужчина, 45 лет, высокая плодовитость, стаж 20 лет",
        "Женщина, 32 года, средняя, стаж 8 лет",
        "Небинарный, 27 лет, низкая плодовитость, стаж 3 года",
        "Врач",
        "Пожарный",
        "Безработный",
        "Сердечная недостаточность",
        "Астма",
        "Иммунитет к радиации",
        "Мутация: ночное зрение",
        "Любит готовить",
        "Играет на гитаре",
        "Парашют",
        "Аптечка",
        "Топор",
        "Арахнофобия",
        "Гидрофобия",
        "Спас котёнка из пожара",
        "Знает азбуку Морзе"
    ];
   
    let isAuthorMode = false;  // по умолчанию только автор может открывать/редактировать/роллить
    const AUTHOR_PASSWORD = "bunker2024";  // можно сменить, автор форума знает
   
    // DOM элементы
    const statsGrid = document.getElementById("statsGrid");
    const nicknameDisplay = document.getElementById("nicknameDisplay");
    const avatarPreview = document.getElementById("avatarPreview");
    const unlockBtn = document.getElementById("unlockBtn");
    const authorPassInput = document.getElementById("authorPass");
    const lockStatusSpan = document.getElementById("lockStatus");
    const authorPanelDiv = document.getElementById("authorPanel");
    const editNickInput = document.getElementById("editNick");
    const editAvatarUrlInput = document.getElementById("editAvatarUrl");
    const updateBasicBtn = document.getElementById("updateBasicBtn");
    const rollStatBtn = document.getElementById("rollStatBtn");
    const statToRollSelect = document.getElementById("statToRoll");
    const poolTextarea = document.getElementById("poolTextarea");
    const addToPoolBtn = document.getElementById("addToPoolBtn");
   
    // Заполнить текстовое поле пулом
    function updatePoolTextarea() {
        poolTextarea.value = characteristicsPool.join("\n");
    }
   
    // Загрузить пул из textarea
    function loadPoolFromTextarea() {
        const text = poolTextarea.value;
        const lines = text.split(/\r?\n/).filter(l => l.trim().length > 0);
        if(lines.length > 0) characteristicsPool = lines;
        else characteristicsPool = ["Пример: Мужчина, 30 лет"];
        updatePoolTextarea();
    }
   
    // Добавить новую строку в пул
    function addLineToPool() {
        const newLine = prompt("Введите новую характеристику (строка для пула):");
        if(newLine && newLine.trim()) {
            characteristicsPool.push(newLine.trim());
            updatePoolTextarea();
            alert(`Добавлено! Всего в пуле: ${characteristicsPool.length}`);
        }
    }
   
    // Ролл для конкретного поля
    function rollStatForKey(key) {
        if(!isAuthorMode) {
            alert("Только автор сообщения может роллить характеристики!");
            return false;
        }
        if(!characteristicsPool.length) {
            alert("Пул характеристик пуст! Добавьте строки в текстовое поле.");
            return false;
        }
        const randomIndex = Math.floor(Math.random() * characteristicsPool.length);
        const newValue = characteristicsPool[randomIndex];
        if(charData[key]) {
            charData[key].value = newValue;
            renderStats();
            return true;
        }
        return false;
    }
   
    // Рендер всех характеристик с учетом закрытости/открытости
    function renderStats() {
        const orderedKeys = [
            "base", "profession", "health", "bio", "hobby", "baggage", "phobia", "fact1", "fact2"
        ];
        const labels = {
            base: "🧬 Пол, возраст, плодовитость, стаж",
            profession: "💼 Профессия",
            health: "❤️ Здоровье",
            bio: "🧬 Биологические особенности",
            hobby: "🎨 Хобби",
            baggage: "🎒 Багаж / Инвентарь",
            phobia: "😨 Фобии",
            fact1: "📌 Факт 1",
            fact2: "📌 Факт 2"
        };
        statsGrid.innerHTML = "";
        for(let key of orderedKeys) {
            const data = charData[key];
            const isOpen = data.open;
            const label = labels[key];
            let valueHtml = "";
            if(isOpen) {
                valueHtml = `<span class="stat-value">${escapeHtml(data.value)}</span> <span class="open-badge">🔓 открыто</span>`;
            } else {
                valueHtml = `<span class="closed" data-key="${key}">[НАЖМИТЕ, ЧТОБЫ ОТКРЫТЬ]</span> <span style="font-size:11px; color:#6c757d;">(закрыто)</span>`;
            }
            const statDiv = document.createElement("div");
            statDiv.className = "stat-item";
            statDiv.innerHTML = `
                <div><span class="stat-label">${label}:</span> ${valueHtml}</div>
            `;
            // Добавим возможность для автора открывать по клику
            const closedSpan = statDiv.querySelector(".closed");
            if(closedSpan && !isOpen) {
                closedSpan.style.cursor = "pointer";
                closedSpan.addEventListener("click", (e) => {
                    e.stopPropagation();
                    if(!isAuthorMode) {
                        alert("❌ Открыть характеристику может только автор сообщения. Введите пароль редактора.");
                        return;
                    }
                    // открываем эту характеристику
                    charData[key].open = true;
                    renderStats();
                });
            }
            statsGrid.appendChild(statDiv);
        }
    }
   
    // простой escape
    function escapeHtml(str) {
        return String(str).replace(/[&<>]/g, function(m) {
            if(m === '&') return '&amp;';
            if(m === '<') return '&lt;';
            if(m === '>') return '&gt;';
            return m;
        }).replace(/[\uD800-\uDBFF][\uDC00-\uDFFF]/g, function(c) {
            return c;
        });
    }
   
    // обновить ник и аватар
    function updateNickAndAvatar() {
        if(!isAuthorMode) {
            alert("Редактирование доступно только автору сообщения!");
            return;
        }
        let newNick = editNickInput.value.trim();
        if(newNick !== "") currentNick = newNick;
        nicknameDisplay.innerText = currentNick;
       
        let avatarUrl = editAvatarUrlInput.value.trim();
        if(avatarUrl !== "") {
            currentAvatarUrl = avatarUrl;
            avatarPreview.innerHTML = `<img src="${escapeHtml(avatarUrl)}" style="width:100%;height:100%;object-fit:cover;" onerror="this.onerror=null;this.src='';this.parentElement.innerHTML='🖼️';this.parentElement.style.background='#e2e8f0';this.parentElement.style.display='flex';this.parentElement.style.alignItems='center';this.parentElement.style.justifyContent='center';">`;
        } else {
            currentAvatarUrl = "";
            avatarPreview.innerHTML = "🖼️";
        }
    }
   
    // Загрузка начальных значений в поля редактирования
    function loadCurrentToEditFields() {
        editNickInput.value = currentNick;
        editAvatarUrlInput.value = currentAvatarUrl;
    }
   
    // Инициализация и обновление всех визуалов
    function fullRefresh() {
        nicknameDisplay.innerText = currentNick;
        if(currentAvatarUrl) {
            avatarPreview.innerHTML = `<img src="${escapeHtml(currentAvatarUrl)}" style="width:100%;height:100%;object-fit:cover;" onerror="this.onerror=null;this.src='';this.parentElement.innerHTML='🖼️';">`;
        } else {
            avatarPreview.innerHTML = "🖼️";
        }
        renderStats();
        loadCurrentToEditFields();
    }
   
    // Установка режима автора
    function setAuthorMode(enable) {
        isAuthorMode = enable;
        if(enable) {
            lockStatusSpan.innerHTML = "🔓 Режим автора (можно редактировать/открывать/роллить)";
            lockStatusSpan.style.color = "#2c7da0";
            authorPanelDiv.style.display = "block";
        } else {
            lockStatusSpan.innerHTML = "🔒 Режим только для чтения. Карточки изменяет автор.";
            lockStatusSpan.style.color = "#a00";
            authorPanelDiv.style.display = "none";
        }
    }
   
    // Проверка пароля
    function unlockAuthor() {
        const pwd = authorPassInput.value;
        if(pwd === AUTHOR_PASSWORD) {
            setAuthorMode(true);
            alert("Доступ автора получен! Теперь вы можете открывать характеристики, роллить, менять ник и аватар.");
        } else {
            alert("Неверный пароль. Редактирование невозможно.");
            setAuthorMode(false);
        }
    }
   
    // Ролл для выбранного поля через интерфейс
    function rollSelectedStat() {
        if(!isAuthorMode) {
            alert("Только автор может выполнять ролл характеристик.");
            return;
        }
        const field = statToRollSelect.value;
        rollStatForKey(field);
    }
   
    // Добавление в пул
    function addToPoolHandler() {
        if(!isAuthorMode) {
            alert("Только автор может пополнять пул характеристик.");
            return;
        }
        addLineToPool();
    }
   
    // Инициализация
    function init() {
        updatePoolTextarea();
        fullRefresh();
        // установим изначально не авторский режим
        setAuthorMode(false);
        // Подписки
        unlockBtn.addEventListener("click", unlockAuthor);
        updateBasicBtn.addEventListener("click", () => {
            if(!isAuthorMode) { alert("Неавторизован!"); return; }
            updateNickAndAvatar();
        });
        rollStatBtn.addEventListener("click", rollSelectedStat);
        addToPoolBtn.addEventListener("click", addToPoolHandler);
        // при изменении текста пула, если автор, можно перезалить массив. Добавим кнопку "Обновить пул"
        const syncPoolBtn = document.createElement("button");
        syncPoolBtn.innerText = "📥 Загрузить пул из текста";
        syncPoolBtn.style.background = "#4b6a8b";
        syncPoolBtn.classList.add("small-btn");
        syncPoolBtn.style.marginLeft = "10px";
        document.querySelector(".flex-btns").appendChild(syncPoolBtn);
        syncPoolBtn.addEventListener("click", () => {
            if(!isAuthorMode) { alert("Доступ только автору"); return; }
            loadPoolFromTextarea();
            alert(`Пул обновлён, строк: ${characteristicsPool.length}`);
        });
       
        // для удобства автосохранение ника и аватарки после изменений от автора в полях
        // по желанию пользователя всё остаётся
    }
   
    init();
</script>
</body>
</html>[/html]

0

226

[html]<!DOCTYPE html>
<html lang="ru">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Бункер: Карточка игрока</title>
    <style>
        body {
            background: #f0f2f5;
            font-family: system-ui, -apple-system, 'Segoe UI', Roboto, sans-serif;
            margin: 0;
            padding: 20px;
        }
        .container {
            max-width: 800px;
            margin: 0 auto;
        }
       
        /* Стили для карточки (видят все) */
        .player-card {
            background: white;
            border-radius: 24px;
            padding: 20px;
            box-shadow: 0 4px 12px rgba(0,0,0,0.08);
            margin-bottom: 20px;
        }
        .row-avatar-nick {
            display: flex;
            gap: 20px;
            align-items: center;
            margin-bottom: 24px;
            flex-wrap: wrap;
        }
        .avatar-box {
            width: 96px;
            height: 96px;
            background: #dee2e6;
            border-radius: 20px;
            display: flex;
            align-items: center;
            justify-content: center;
            overflow: hidden;
            flex-shrink: 0;
            border: 1px solid #ced4da;
        }
        .avatar-box img {
            width: 100%;
            height: 100%;
            object-fit: cover;
        }
        .nickname {
            font-size: 28px;
            font-weight: 700;
            color: #1e2a3e;
        }
        .stats-grid {
            display: flex;
            flex-direction: column;
            gap: 12px;
        }
        .stat-item {
            background: #f8f9fa;
            border-radius: 18px;
            padding: 10px 16px;
            border-left: 5px solid #adb5bd;
            font-size: 14px;
        }
        .stat-label {
            font-weight: 700;
            display: inline-block;
            width: 180px;
            color: #2c3e50;
        }
        .stat-value {
            font-weight: 500;
            color: #0b1c2c;
            word-break: break-word;
        }
        .stat-empty {
            color: #adb5bd;
            font-style: italic;
        }
       
        /* Панель автора (видит только автор) */
        .author-panel {
            background: #fff3cd;
            border: 1px solid #ffecb3;
            border-radius: 20px;
            padding: 20px;
            margin-bottom: 20px;
        }
        .author-panel h3 {
            margin: 0 0 15px 0;
            font-size: 18px;
            color: #856404;
        }
        .author-pass-section {
            margin-bottom: 20px;
            padding-bottom: 15px;
            border-bottom: 1px solid #ffe0a3;
        }
        .author-pass-section input {
            padding: 8px 12px;
            border: 1px solid #ccb;
            border-radius: 10px;
            margin-right: 10px;
        }
        .edit-row {
            margin-bottom: 12px;
            display: flex;
            flex-wrap: wrap;
            align-items: center;
            gap: 10px;
        }
        .edit-row label {
            font-weight: 600;
            width: 180px;
            font-size: 13px;
        }
        .edit-row input, .edit-row textarea {
            flex: 1;
            min-width: 200px;
            padding: 8px 12px;
            border: 1px solid #ddd;
            border-radius: 10px;
            font-family: inherit;
            font-size: 13px;
        }
        .edit-row textarea {
            resize: vertical;
        }
        button {
            background: #2c7da0;
            border: none;
            color: white;
            padding: 10px 20px;
            border-radius: 40px;
            font-size: 14px;
            font-weight: 500;
            cursor: pointer;
            transition: 0.1s;
        }
        button:hover {
            background: #1f5e7a;
        }
        .btn-update {
            background: #28a745;
            margin-top: 10px;
            width: 100%;
        }
        .btn-update:hover {
            background: #218838;
        }
        .btn-roll {
            background: #6c757d;
            font-size: 12px;
            padding: 5px 12px;
        }
        .btn-roll:hover {
            background: #5a6268;
        }
        .roll-group {
            display: flex;
            gap: 8px;
            align-items: center;
            flex-wrap: wrap;
        }
        .pool-section {
            margin-top: 15px;
            padding-top: 15px;
            border-top: 1px solid #ffe0a3;
        }
        .success-msg {
            background: #d4edda;
            color: #155724;
            padding: 10px;
            border-radius: 10px;
            margin-top: 10px;
            text-align: center;
            font-size: 13px;
        }
        .footer-note {
            font-size: 12px;
            color: #6c757d;
            text-align: center;
            margin-top: 20px;
        }
        .lock-icon {
            font-size: 12px;
            margin-left: 8px;
            color: #856404;
        }
    </style>
</head>
<body>
<div class="container">
   
    <!-- КАРТОЧКА ИГРОКА (видят все) -->
    <div class="player-card" id="playerCard">
        <div class="row-avatar-nick">
            <div class="avatar-box" id="avatarBox">
                <div id="avatarPreview">🖼️</div>
            </div>
            <div class="nickname" id="nicknameDisplay">Игрок</div>
        </div>
        <div class="stats-grid" id="statsGrid"></div>
    </div>
   
    <!-- ПАНЕЛЬ АВТОРА (видит только автор после ввода пароля) -->
    <div class="author-panel" id="authorPanel" style="display: none;">
        <h3>✏️ Панель автора — редактирование карточки</h3>
       
        <div class="edit-row">
            <label>👤 Ник игрока:</label>
            <input type="text" id="editNick" placeholder="Введите ник">
        </div>
        <div class="edit-row">
            <label>🖼️ URL аватара:</label>
            <input type="text" id="editAvatar" placeholder="https://... (квадратное изображение)">
        </div>
       
        <div class="edit-row">
            <label>🧬 Пол, возраст, плодовитость, стаж:</label>
            <input type="text" id="editBase" placeholder="Например: Мужчина, 30 лет, высокая, стаж 8 лет">
        </div>
        <div class="edit-row">
            <label>💼 Профессия:</label>
            <input type="text" id="editProfession" placeholder="Профессия">
        </div>
        <div class="edit-row">
            <label>❤️ Здоровье:</label>
            <input type="text" id="editHealth" placeholder="Здоровье">
        </div>
        <div class="edit-row">
            <label>🧬 Биологические особенности:</label>
            <input type="text" id="editBio" placeholder="Биологические особенности">
        </div>
        <div class="edit-row">
            <label>🎨 Хобби:</label>
            <input type="text" id="editHobby" placeholder="Хобби">
        </div>
        <div class="edit-row">
            <label>🎒 Багаж / Инвентарь:</label>
            <input type="text" id="editBaggage" placeholder="Багаж">
        </div>
        <div class="edit-row">
            <label>😨 Фобии:</label>
            <input type="text" id="editPhobia" placeholder="Фобии">
        </div>
        <div class="edit-row">
            <label>📌 Факт 1:</label>
            <input type="text" id="editFact1" placeholder="Интересный факт">
        </div>
        <div class="edit-row">
            <label>📌 Факт 2:</label>
            <input type="text" id="editFact2" placeholder="Ещё факт">
        </div>
       
        <!-- Ролл характеристик -->
        <div class="pool-section">
            <div style="font-weight: 600; margin-bottom: 10px;">🎲 Быстрый ролл из пула</div>
            <div class="roll-group">
                <select id="rollField">
                    <option value="base">Пол, возраст, плодовитость, стаж</option>
                    <option value="profession">Профессия</option>
                    <option value="health">Здоровье</option>
                    <option value="bio">Биологические особенности</option>
                    <option value="hobby">Хобби</option>
                    <option value="baggage">Багаж</option>
                    <option value="phobia">Фобии</option>
                    <option value="fact1">Факт 1</option>
                    <option value="fact2">Факт 2</option>
                </select>
                <button id="rollBtn" class="btn-roll">🎲 Случайная характеристика</button>
            </div>
            <div style="margin-top: 12px;">
                <textarea id="poolTextarea" rows="2" placeholder="Пул характеристик (каждая с новой строки) Например: Мужчина, 34 года, высокая плодовитость, стаж 12 лет Водитель Отличное здоровье Аллергия на пыльцу"></textarea>
                <button id="addToPoolBtn" class="btn-roll" style="margin-top: 5px;">➕ Добавить строку в пул</button>
            </div>
        </div>
       
        <button id="updateCardBtn" class="btn-update">💾 Опубликовать изменения (увидят все!)</button>
        <div id="updateStatus"></div>
    </div>
   
    <!-- Кнопка для входа в режим автора -->
    <div id="loginSection" style="text-align: center; margin-top: 10px;">
        <input type="password" id="authorPassword" placeholder="Пароль автора" style="padding: 8px 12px; border-radius: 10px; border: 1px solid #ccc;">
        <button id="loginBtn">🔓 Войти как автор</button>
    </div>
   
    <div class="footer-note">
        ✅ Автор вносит данные → нажимает «Опубликовать» → все видят обновлённую карточку.
    </div>
</div>

<script>
    // ==================== НАСТРОЙКИ ====================
    // Пароль автора (можно изменить)
    const AUTHOR_PASSWORD = "bunker2024";
   
    // Ключ для хранения данных в localStorage (чтобы данные сохранялись между обновлениями)
    const STORAGE_KEY = 'bunker_card_data';
   
    // Структура характеристик
    const charLabels = {
        nick: 'Ник',
        avatar: 'Аватар',
        base: '🧬 Пол, возраст, плодовитость, стаж',
        profession: '💼 Профессия',
        health: '❤️ Здоровье',
        bio: '🧬 Биологические особенности',
        hobby: '🎨 Хобби',
        baggage: '🎒 Багаж / Инвентарь',
        phobia: '😨 Фобии',
        fact1: '📌 Факт 1',
        fact2: '📌 Факт 2'
    };
   
    const charFields = ['base', 'profession', 'health', 'bio', 'hobby', 'baggage', 'phobia', 'fact1', 'fact2'];
   
    // Данные карточки (по умолчанию пустые)
    let cardData = {
        nick: '',
        avatar: '',
        base: '',
        profession: '',
        health: '',
        bio: '',
        hobby: '',
        baggage: '',
        phobia: '',
        fact1: '',
        fact2: ''
    };
   
    // Пул характеристик для ролла
    let characteristicsPool = [
        "Мужчина, 45 лет, высокая плодовитость, стаж 20 лет",
        "Женщина, 32 года, средняя, стаж 8 лет",
        "Мужчина, 27 лет, низкая плодовитость, стаж 3 года",
        "Врач-хирург",
        "Пожарный",
        "Инженер-строитель",
        "Отличное здоровье, выносливость",
        "Сердечная недостаточность",
        "Астма, нужен ингалятор",
        "Мутация: ночное зрение",
        "Аллергия на пыльцу",
        "Коллекционирует марки",
        "Играет на гитаре",
        "Любит готовить",
        "Рюкзак с консервами и водой",
        "Аптечка и бинты",
        "Топор и верёвка",
        "Арахнофобия (боязнь пауков)",
        "Клаустрофобия",
        "Спас котёнка из пожара",
        "Знает азбуку Морзе",
        "Был в армии"
    ];
   
    // Текущий режим автора
    let isAuthorMode = false;
   
    // DOM элементы
    const statsGrid = document.getElementById('statsGrid');
    const nicknameDisplay = document.getElementById('nicknameDisplay');
    const avatarPreview = document.getElementById('avatarPreview');
    const authorPanel = document.getElementById('authorPanel');
    const loginSection = document.getElementById('loginSection');
    const authorPasswordInput = document.getElementById('authorPassword');
    const loginBtn = document.getElementById('loginBtn');
    const updateCardBtn = document.getElementById('updateCardBtn');
    const updateStatus = document.getElementById('updateStatus');
    const rollBtn = document.getElementById('rollBtn');
    const rollField = document.getElementById('rollField');
    const poolTextarea = document.getElementById('poolTextarea');
    const addToPoolBtn = document.getElementById('addToPoolBtn');
   
    // Поля ввода
    const editNick = document.getElementById('editNick');
    const editAvatar = document.getElementById('editAvatar');
    const editBase = document.getElementById('editBase');
    const editProfession = document.getElementById('editProfession');
    const editHealth = document.getElementById('editHealth');
    const editBio = document.getElementById('editBio');
    const editHobby = document.getElementById('editHobby');
    const editBaggage = document.getElementById('editBaggage');
    const editPhobia = document.getElementById('editPhobia');
    const editFact1 = document.getElementById('editFact1');
    const editFact2 = document.getElementById('editFact2');
   
    // ========== Загрузка и сохранение данных ==========
    function loadData() {
        const saved = localStorage.getItem(STORAGE_KEY);
        if (saved) {
            try {
                const parsed = JSON.parse(saved);
                for (let key in cardData) {
                    if (parsed[key] !== undefined) cardData[key] = parsed[key];
                }
            } catch(e) {}
        }
       
        // Загружаем пул
        const savedPool = localStorage.getItem('bunker_pool');
        if (savedPool) {
            try {
                characteristicsPool = JSON.parse(savedPool);
            } catch(e) {}
        }
        updatePoolTextarea();
    }
   
    function saveData() {
        localStorage.setItem(STORAGE_KEY, JSON.stringify(cardData));
        localStorage.setItem('bunker_pool', JSON.stringify(characteristicsPool));
    }
   
    // ========== Отрисовка карточки для всех ==========
    function renderCard() {
        // Ник и аватар
        nicknameDisplay.innerText = cardData.nick || 'Без имени';
        if (cardData.avatar && cardData.avatar.trim()) {
            avatarPreview.innerHTML = `<img src="${escapeHtml(cardData.avatar)}" style="width:100%;height:100%;object-fit:cover;" onerror="this.onerror=null;this.src='';this.parentElement.innerHTML='🖼️';">`;
        } else {
            avatarPreview.innerHTML = '🖼️';
        }
       
        // Характеристики
        statsGrid.innerHTML = '';
        for (let field of charFields) {
            const value = cardData[field];
            const label = charLabels[field];
            const statDiv = document.createElement('div');
            statDiv.className = 'stat-item';
           
            let valueHtml;
            if (value && value.trim()) {
                valueHtml = `<span class="stat-value">${escapeHtml(value)}</span>`;
            } else {
                valueHtml = `<span class="stat-empty">— не указано —</span>`;
            }
           
            statDiv.innerHTML = `
                <div>
                    <span class="stat-label">${label}:</span>
                    ${valueHtml}
                </div>
            `;
            statsGrid.appendChild(statDiv);
        }
    }
   
    // ========== Обновление карточки из формы автора ==========
    function updateCardFromAuthor() {
        cardData.nick = editNick.value.trim();
        cardData.avatar = editAvatar.value.trim();
        cardData.base = editBase.value.trim();
        cardData.profession = editProfession.value.trim();
        cardData.health = editHealth.value.trim();
        cardData.bio = editBio.value.trim();
        cardData.hobby = editHobby.value.trim();
        cardData.baggage = editBaggage.value.trim();
        cardData.phobia = editPhobia.value.trim();
        cardData.fact1 = editFact1.value.trim();
        cardData.fact2 = editFact2.value.trim();
       
        saveData();
        renderCard();
       
        // Показать сообщение об успехе
        updateStatus.innerHTML = '<div class="success-msg">✅ Карточка обновлена! Все пользователи видят новые данные.</div>';
        setTimeout(() => {
            updateStatus.innerHTML = '';
        }, 3000);
    }
   
    // Загрузить текущие данные в форму автора
    function loadDataToAuthorForm() {
        editNick.value = cardData.nick || '';
        editAvatar.value = cardData.avatar || '';
        editBase.value = cardData.base || '';
        editProfession.value = cardData.profession || '';
        editHealth.value = cardData.health || '';
        editBio.value = cardData.bio || '';
        editHobby.value = cardData.hobby || '';
        editBaggage.value = cardData.baggage || '';
        editPhobia.value = cardData.phobia || '';
        editFact1.value = cardData.fact1 || '';
        editFact2.value = cardData.fact2 || '';
    }
   
    // ========== Ролл характеристик ==========
    function updatePoolTextarea() {
        poolTextarea.value = characteristicsPool.join('\n');
    }
   
    function loadPoolFromTextarea() {
        const text = poolTextarea.value;
        const lines = text.split(/\r?\n/).filter(l => l.trim().length > 0);
        if (lines.length > 0) {
            characteristicsPool = lines;
        } else {
            characteristicsPool = ["Пример характеристики"];
        }
        updatePoolTextarea();
        localStorage.setItem('bunker_pool', JSON.stringify(characteristicsPool));
    }
   
    function addToPool() {
        const newItem = prompt("Введите новую характеристику для пула:");
        if (newItem && newItem.trim()) {
            characteristicsPool.push(newItem.trim());
            updatePoolTextarea();
            localStorage.setItem('bunker_pool', JSON.stringify(characteristicsPool));
            alert(`✅ Добавлено! В пуле ${characteristicsPool.length} характеристик.`);
        }
    }
   
    function rollCharacteristic() {
        if (!isAuthorMode) {
            alert("Только автор может использовать ролл!");
            return;
        }
       
        if (characteristicsPool.length === 0) {
            alert("Пул характеристик пуст! Добавьте варианты в текстовое поле.");
            return;
        }
       
        const field = rollField.value;
        const randomIndex = Math.floor(Math.random() * characteristicsPool.length);
        const randomValue = characteristicsPool[randomIndex];
       
        // Заполняем соответствующее поле ввода
        switch(field) {
            case 'base': editBase.value = randomValue; break;
            case 'profession': editProfession.value = randomValue; break;
            case 'health': editHealth.value = randomValue; break;
            case 'bio': editBio.value = randomValue; break;
            case 'hobby': editHobby.value = randomValue; break;
            case 'baggage': editBaggage.value = randomValue; break;
            case 'phobia': editPhobia.value = randomValue; break;
            case 'fact1': editFact1.value = randomValue; break;
            case 'fact2': editFact2.value = randomValue; break;
        }
       
        alert(`🎲 Выпало: ${randomValue}`);
    }
   
    // ========== Режим автора ==========
    function enableAuthorMode() {
        isAuthorMode = true;
        authorPanel.style.display = 'block';
        loginSection.style.display = 'none';
        loadDataToAuthorForm();
    }
   
    function checkPassword() {
        const pwd = authorPasswordInput.value;
        if (pwd === AUTHOR_PASSWORD) {
            enableAuthorMode();
        } else {
            alert("❌ Неверный пароль!");
            authorPasswordInput.value = '';
        }
    }
   
    // ========== Инициализация ==========
    function init() {
        loadData();
        renderCard();
       
        // Обработчики
        loginBtn.addEventListener('click', checkPassword);
        updateCardBtn.addEventListener('click', updateCardFromAuthor);
        rollBtn.addEventListener('click', rollCharacteristic);
        addToPoolBtn.addEventListener('click', addToPool);
       
        // Синхронизация пула из textarea (по кнопке, добавим кнопку обновления пула)
        const syncPoolBtn = document.createElement('button');
        syncPoolBtn.innerText = '📥 Загрузить пул из текста';
        syncPoolBtn.className = 'btn-roll';
        syncPoolBtn.style.marginTop = '5px';
        syncPoolBtn.style.marginLeft = '10px';
        document.querySelector('.pool-section').appendChild(syncPoolBtn);
        syncPoolBtn.addEventListener('click', () => {
            if (!isAuthorMode) { alert("Доступ только автору"); return; }
            loadPoolFromTextarea();
            alert(`Пул обновлён! Всего строк: ${characteristicsPool.length}`);
        });
       
        // Нажатие Enter в поле пароля
        authorPasswordInput.addEventListener('keypress', (e) => {
            if (e.key === 'Enter') checkPassword();
        });
    }
   
    function escapeHtml(str) {
        if (!str) return '';
        return String(str).replace(/[&<>]/g, function(m) {
            if (m === '&') return '&amp;';
            if (m === '<') return '&lt;';
            if (m === '>') return '&gt;';
            return m;
        });
    }
   
    init();
</script>
</body>
</html>[/html]

0


Вы здесь » design © damask.beresklet » Тестовый форум » Тестовое сообщение


Рейтинг форумов | Создать форум бесплатно