Compare commits
19
Commits
49edfdca66
..
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
137b8f5eb7 | ||
|
|
3dbabfbf0c | ||
|
|
0213eb1ca3 | ||
|
|
fa7ae5069a | ||
|
|
3be844ffb6 | ||
|
|
bf90f3d4c2 | ||
|
|
ca1a01a06e | ||
|
|
387b3925c6 | ||
|
|
3c3055eb1f | ||
|
|
85ebad491a | ||
|
|
5ad296e61e | ||
|
|
a08d3c2e4e | ||
|
|
e22032dab6 | ||
|
|
2728d8511a | ||
|
|
352100e49c | ||
|
|
8b7d6216a4 | ||
|
|
da66d2855a | ||
|
|
1ec33d6a80 | ||
|
|
0ca319ae0f |
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"presets": ["@babel/preset-env", "@babel/preset-typescript"],
|
||||
"plugins": ["@babel/plugin-transform-optional-chaining"]
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
{
|
||||
"ignorePatterns": [
|
||||
"node_modules/",
|
||||
"settings.*.js",
|
||||
"webpack.*.js"
|
||||
],
|
||||
"plugins": ["@typescript-eslint"],
|
||||
"extends": ["eslint:recommended", "plugin:@typescript-eslint/recommended", "google"],
|
||||
"parser": "@babel/eslint-parser",
|
||||
"globals": {
|
||||
"window": true,
|
||||
"$": true,
|
||||
"jQuery": true
|
||||
},
|
||||
"env": {
|
||||
"browser": true
|
||||
},
|
||||
"rules": {
|
||||
"no-undef": "off",
|
||||
"getter-return": "off",
|
||||
"indent": "off",
|
||||
"no-unused-vars": "off",
|
||||
"new-cap": "off",
|
||||
"no-debugger": "off",
|
||||
"@typescript-eslint/no-unused-vars": "off"
|
||||
}
|
||||
}
|
||||
+1
-1
@@ -1,2 +1,2 @@
|
||||
node_modules
|
||||
build*.sh
|
||||
*.sh
|
||||
|
||||
@@ -1,40 +0,0 @@
|
||||
# Список реализованых шуток
|
||||
|
||||
## `shake` - Форумо-трясение
|
||||
Страница форума начинает трястись
|
||||
|
||||
Настройки:
|
||||
* `chance` - Вероятность срабатывания в процентах (по умолчанию: `1%`)
|
||||
* `directions` - Направление тряски (по умолчанию: `['top', 'left', 'bottom', 'left']`)
|
||||
* `selector` - Селектор всех элементов, которые будут трястись (по умолчанию: `[id^=pun]`)
|
||||
* `interval` - Частота тряски (по умолчанию: `100`)
|
||||
* `force` - Сила тряски (по умолчанию: `0.5`)
|
||||
|
||||
## `mirror` - Зеркало
|
||||
Элементы форума отражаются (по крайней мере, пытаются)
|
||||
|
||||
Настройки:
|
||||
* `chance` - Вероятность срабатывания в процентах (по умолчанию: `30%`)
|
||||
|
||||
## `carnival` - Карнавал
|
||||
Аватарки приобретают интересные цвета
|
||||
|
||||
Настройки:
|
||||
* `chance` - Вероятность срабатывания в процентах (по умолчанию: `30%`)
|
||||
* `selector` - Селектор всех элементов, которые будут перекрашены (по умолчанию: `img[src*="/avatars/"]`)
|
||||
* `variant_chance` - Вероятность применения некоторого фильтра (по умолчанию: `20%`)
|
||||
* `variants` - Список фильтров и диапозона случайных параметров
|
||||
|
||||
## `humanitarian_hell` - Ад гуманитария
|
||||
Все "тся" заменяются на "ться", а все "ться" на "тся"
|
||||
|
||||
Настройки:
|
||||
* `chance` - Вероятность срабатывания в процентах (по умолчанию: `20%`)
|
||||
* `selector` - Селектор всех элементов, которые будут затронуты: `.post-content`)
|
||||
|
||||
## `crazy_letters` - Мама, Я сОшЛа С уМа!
|
||||
иМеНа ЗаБоРчИкОм
|
||||
|
||||
Настройки:
|
||||
* `chance` - Вероятность срабатывания в процентах (по умолчанию: `20%`)
|
||||
* `selector` - Селектор всех элементов, которые будут затронуты: `.pa-author a`)
|
||||
+17
@@ -0,0 +1,17 @@
|
||||
# [Условия использования бесплатного контента](https://forumd.ru/viewtopic.php?id=5751#rule)
|
||||
|
||||
1. Данные правила относятся к бесплатным скриптам, кодам, шаблонам, графике, а также гайдам, сделанным в рамках проекта [ForumD.ru](https://forumd.ru/).
|
||||
Под данные правила попадают [разработки специалистов ForumD.ru](https://forumd.ru/viewforum.php?id=33) (в том числе бывших, сделанные в период участия в нашем проекте), а также отдельных пользователей форума, пожелавших применять к своему бесплатному контенту данные правила использования и распространения.
|
||||
|
||||
2. Весь бесплатный контент, сделанный в рамках проекта ForumD.ru, можно свободно использовать любых целях.
|
||||
|
||||
3. При этом, весь производный контент (модификации скриптов, стилевых решений, макетов, гайды и уроки по нашим работам и т.п.), выложенный в открытый доступ, должен распространяться на таких же свободных условиях.
|
||||
Администрация оставляет за собой право цитировать производный контент, выложенный в открытый доступ, с указанием авторства и ссылки на источник.
|
||||
|
||||
4. При публикации авторских работ, где использовался наш контент, на любых ресурсах, необходимо указывать использованный в работе бесплатный контент и автора с **кликабельной прямой ссылкой на топик с контентом. В случае отсутствия ссылок на использованный бесплатный контент, администрация форума оставляет за собой право размещать в публичном доступе производный от нашего контент, использованный в авторской работе.
|
||||
|
||||
5. Также можно цитировать любой бесплатный контент на других сайтах, форумах, блогах в социальных сетях и т.д. с указанием автора и ссылки на [ForumD.ru](https://forumd.ru/). В случае цитирования наших работ, статей, уроков или гайдов на других площадках, мы оставляем за собой право цитировать ваш контент в ответ, естественно, с указанием авторства и ссылки на источник.
|
||||
|
||||
6. Необязательное, но очень приятное для нас условие - обратная связь. Взяли? В теме скрипта, кода, макета или графики напишите "спасибо" и укажите ссылку где используется или цитируется наша разработка.
|
||||
|
||||
7. Вся [команда и мастера ForumD](https://forumd.ru/pages/contacts) обязуются соблюдать свои же условия.
|
||||
@@ -1,50 +1,238 @@
|
||||
# Бибилотека первоапрельских розыгрышей для форумов MyBB
|
||||
# Библиотека первоапрельских розыгрышей для форумов MyBB
|
||||
|
||||
Базовое использование:
|
||||
|
||||
```html
|
||||
<!-- Первоапрельские шутки -->
|
||||
<script src="https://static.kozhilya.ru/script/april-fools.min.js"></script>
|
||||
<script>
|
||||
// Установка настроек системы
|
||||
window.AprilJokes.settings = {
|
||||
// ...
|
||||
};
|
||||
// Установка настроек системы
|
||||
window.AprilJokes.settings = {
|
||||
// ...
|
||||
};
|
||||
</script>
|
||||
<!-- Конец первоапрельских шуток -->
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Полный список шуток
|
||||
|
||||
- `shake` — **Форумо-трясение**. Страница форума начинает трястись.
|
||||
- `mirror` — **Зеркало**. Элементы форума отражаются... По крайней мере, пытаются.
|
||||
- `carnival` — **Карнавал**. Аватарки приобретают интересные цвета.
|
||||
- `humanitarian_hell` — **Ад гуманитария**. Все "тся" заменяются на "ться", а все "ться" на "тся".
|
||||
- `crazy_letters` — **Мама, Я сОшЛа С уМа!** иМеНа ЗаБоРчИкОм
|
||||
- `letter_chaos` — **Буквенный хаос**. Все буквы в словах, кроме первой и последней, перемешены.
|
||||
- `cursor` — **Красивые курсоры**. Эффекты, следующие за курсором.
|
||||
- `stupid_title` — **Глупые статусы**. Каждому пользователю даётся новый глупый статус.
|
||||
- `inverted_scroll` — **Обратная гравитация**. Скролл в обратную сторону.
|
||||
- `latinization` — **Латинизация**. Все символы на странице или только в постах или еще где-то местами - латиницей вместо кириллицы и наоборот
|
||||
|
||||
---
|
||||
|
||||
## Тестирование шуток
|
||||
|
||||
Что бы проверить работу системы, вы можете включить "режим тестера". Для этого добавьте свой `UserID` в настройку `core.testers`:
|
||||
|
||||
```js
|
||||
window.AprilJokes.settings = {
|
||||
// ...
|
||||
core: {
|
||||
testers: [ 2, 3, 1186 ],
|
||||
},
|
||||
// ...
|
||||
};
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Настройка шуток
|
||||
|
||||
Настройка шуток происходит при присвоении объекта `window.AprilJokes.settings`.
|
||||
Это присвоение должно произойти **после** загрузки скрипта.
|
||||
|
||||
Для всех шуток определены 2 общих поля:
|
||||
|
||||
```js
|
||||
window.AprilJokes.settings = {
|
||||
// ...
|
||||
example: {
|
||||
/**
|
||||
* Активность шутки.
|
||||
* Установите это значение `false`, что бы отключить шутку.
|
||||
* Если `true`, то шутка будет обработана как обычно.
|
||||
*/
|
||||
enabled: true,
|
||||
|
||||
/**
|
||||
* Вероятность срабатывания в процентах.
|
||||
* Если установлено значение `100`, шутка будет срабатывать всегда.
|
||||
* Если установлено значение `0`, шутка никогда не сработает.
|
||||
* Лучше отключать шутку, используя enabled = false!
|
||||
*/
|
||||
chance: 20,
|
||||
},
|
||||
// ...
|
||||
};
|
||||
```
|
||||
|
||||
Полный список настроек с объяснениями вы можете посмотреть в файле [`settings.example.js`](./settings.example.js). Также представлен файл [`settings.compact.js`](./settings.compact.js), который также содержит все настройки, но без комментариев.
|
||||
|
||||
При этом, объективно, вставлять весь этот код в HTML-начало может быть "немного" неудобно, поэтому вы можете скачать код (кнопка "Raw" над файлом), изменить его, и загрузить в файлы форума, подключив скрипт таким образом:
|
||||
|
||||
```html
|
||||
<!-- Первоапрельские шутки -->
|
||||
<script src="https://static.kozhilya.ru/script/april-fools.min.js"></script>
|
||||
<script src="https://forumstatic.ru/files/$$$$/$$/$$/$$$$$.js"></script>
|
||||
<!-- Конец первоапрельских шуток -->
|
||||
```
|
||||
|
||||
Заметьте, что во всех примерах приведены значения настроек по умолчанию. Вы _можете_ удалять строки, значения по умолчанию которых вас устраивают.
|
||||
|
||||
Если вас интересует только несколько шуток, вы можете использовать метод `disableAll()`, что бы выключить выполнение всех шуток, а потом включать отдельные шутки через установку настройки `enabled`:
|
||||
|
||||
```js
|
||||
window.AprilJokes.disableAll();
|
||||
|
||||
window.AprilJokes.settings = {
|
||||
// ...
|
||||
example: {
|
||||
enabled: true,
|
||||
// ...
|
||||
},
|
||||
// ...
|
||||
};
|
||||
```
|
||||
|
||||
Обратите внимание, что метод `disableAll()` должен быть вызван _до_ установки настроек `settings`, иначе заданные значения `enabled` будут сброшены.
|
||||
|
||||
Так же есть метод `window.AprilJokes.enableAll()`, который включает все шутки, в том числе отключённые по умолчанию.
|
||||
|
||||
---
|
||||
|
||||
### Настройка `shake` (Форумо-трясение)
|
||||
|
||||
Тряска элементов происходит путём изменения перечисленных направлений `margin` на значение ±`force` от стандартного значения.
|
||||
|
||||
> **Важно**: эта шутка может вызвать дискомфорт у пользователей. Настоятельно рекомендуется оставить вероятность срабатывания шутки (`chance`) очень низкой, что бы у пользователей была возможность избавиться от шутки простым обновлением страницы! Вариант отключить шутку (`enabled: false`) тоже вариант.
|
||||
|
||||
---
|
||||
|
||||
### Настройка `mirror` (Зеркало)
|
||||
|
||||
Элементы форума отражаются по весьма специфичному алгоритму... И из-за этого, шутка может работать _очень_ плохо на некоторых дизайнах, поэтому данная шутка по умолчанию **отключена**
|
||||
|
||||
Для того, что бы попробовать, сработает ли шутка для вашего дизайна, используйте в консоли разработчика:
|
||||
|
||||
```js
|
||||
window.AprilJokes.start("mirror", true);
|
||||
```
|
||||
|
||||
Так же можете [посмотреть](./src/jokes/mirror.ts#L16) на метод `MirrorJoke.start`, что бы узнать как именно происходит отражение.
|
||||
|
||||
---
|
||||
|
||||
### Настройка `cursor` (Красивые курсоры)
|
||||
|
||||
Используются курсоры, предложенные в библотеке `tholman/cursor-effects` от Tim Holman ([Github](https://github.com/tholman/cursor-effects), [сайт](https://tholman.com/cursor-effects/)).
|
||||
|
||||
Код предусматривает, что одновременно активировано не более 1 курсора из этой библиотеки. Так же для каждого курсора установлен свой шанс срабатывания, однако из-за необходимости активации строго одного эффекта, предусмотрено использование весового коэффициента `weight`: если говорить просто, то чем больше это значение, то тем больше шанс срабатывания эффекта относительно других эффектов.
|
||||
|
||||
Так же для каждого эффекта определена настройка `enabled`, позволяющая отключить эффекты, и метод `disableAll` для отключения эффектов:
|
||||
|
||||
```js
|
||||
window.AprilJokes.jokes.cursor.disableAll();
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Настройка `stupid_title` (Глупые статусы)
|
||||
|
||||
> **Важно**: этот блок может быть чрезвычайно капризным из-за особенностей хранения данных, особенно блок `variants`. Изменяя его, строго следуйте этой инструкции, иначе скрипт может перестать работать ожидаемым образом.
|
||||
|
||||
Данная шутка генерирует статус из случайных слов. За счёт использования псевдослучайного генератора, хеширования и привязки к времени UTC, ожидается что все пользователи одновременно будут видеть один и тот же статус для каждого пользователя, при этом он меняется каждые `change_frequency` минут. Установите это значение равным `0`, что бы статус генерировался при каждом обновлении страницы.
|
||||
|
||||
Для настройки списка слов используется функция `BuildStupdiTitleVariant`, которая преобразует список слов во внутренний формат скрипта. Настоятельно рекомендуется использовать именно её, а не записывать параметры напрямую:
|
||||
|
||||
```js
|
||||
window.AprilJokes.settings = {
|
||||
// ...
|
||||
stupid_title: {
|
||||
// ...
|
||||
variants: {
|
||||
adj: {
|
||||
...window.AprilJokes.settings.stupid_title.variants.adj,
|
||||
...BuildStupidTitleVariant(true, "скромный, шутливый, !грубый"),
|
||||
...BuildStupidTitleVariant(false, "скромная, шутливая, !грубая"),
|
||||
},
|
||||
noun: {...window.AprilJokes.settings.stupid_title.variants.noun},
|
||||
verb: {...window.AprilJokes.settings.stupid_title.variants.verb},
|
||||
place: {...window.AprilJokes.settings.stupid_title.variants.place},
|
||||
adv: {...window.AprilJokes.settings.stupid_title.variants.adv},
|
||||
p: {...window.AprilJokes.settings.stupid_title.variants.p},
|
||||
},
|
||||
// ...
|
||||
},
|
||||
// ...
|
||||
};
|
||||
```
|
||||
|
||||
Обратите внимание на конструкции вида `{ ...something }` — эта конструкция позволяет перенести все поля нескольких старых объектов в один новый. При этом, если в двух объединяемых объектах есть совпадающие значения, то будет сохранено последнее.
|
||||
|
||||
Говоря проще: добавляйте ваши настройки _после_ строки вида `...window.AprilJokes.settings.stupid_title.variants.adj`, что бы ваши настройки не были перезаписаны.
|
||||
|
||||
Метод `BuildStupdiTitleVariant` принимает 2 аргумента:
|
||||
|
||||
- `gender` — определённый род для генерации. Используйте `true` для мужского рода, `false` для женского и `null`, если добавляемые варианты могут использоваться в обоих случаях.
|
||||
- `words` — строка, со списком вариантов через запятую. В качестве вариантов могут быть слова из русских или английских букв, пробелов или дефисов, то есть можно использовать `"..., почему-то и зачем-то , ..."` и `почему-то и зачем-то` будет воспринят как один вариант. Также варианты можно модифицировать, добавив один или несколько модификаторов:
|
||||
- `!` отключает использование слова. Этим модификатором можно отключать слова из списка по умолчанию (см `!грубый` в примере выше).
|
||||
- `%` помечает слово как редкое. Шанс его использования понижается в 2 раза.
|
||||
- `^` помечает слово как частое. Шанс его использования повышается в 2 раза.
|
||||
- `$` помечает слово как грубое. Такие слова не будут отображаться, если в настройках отключена соответсвующая настройка `rude`.
|
||||
|
||||
Полный список слов по умолчанию вы можете посмотреть [в Google Таблице](https://docs.google.com/spreadsheets/d/1MJIfENqy7a9DczRj38c34t27ZQtEmBSi/edit?usp=sharing&ouid=112152128398997742033&rtpof=true&sd=true) или [в исходном файле](./src/data/stupid_title_variants.ts).
|
||||
|
||||
---
|
||||
|
||||
## Разработка дополнений
|
||||
|
||||
Использование для тестирования:
|
||||
|
||||
```html
|
||||
<!-- Первоапрельские шутки: версия для разработчиков -->
|
||||
<script src="https://static.kozhilya.ru/script/april-fools.js"></script>
|
||||
<script>
|
||||
// Установка настроек системы
|
||||
window.AprilJokes.settings = {
|
||||
// ...
|
||||
};
|
||||
// Установка настроек системы
|
||||
window.AprilJokes.settings = {
|
||||
// ...
|
||||
};
|
||||
|
||||
// Создание новой шутки
|
||||
window.AprilJokes.addCustom(
|
||||
// ID шутки
|
||||
'alert',
|
||||
// Создание новой шутки
|
||||
window.AprilJokes.addCustom(
|
||||
// ID шутки
|
||||
"alert",
|
||||
|
||||
// Вызываемая функция шутки
|
||||
settings => alert(settings.text),
|
||||
// Вызываемая функция шутки
|
||||
(settings) => {
|
||||
alert(settings.text);
|
||||
},
|
||||
|
||||
// Стандартные настройки
|
||||
{
|
||||
chance: 30,
|
||||
text: 'Fooled!',
|
||||
}
|
||||
);
|
||||
// Стандартные настройки
|
||||
{
|
||||
title: 'Название шутки',
|
||||
description: 'Описание шутки',
|
||||
enabled: true,
|
||||
chance: 30,
|
||||
text: "Fooled!",
|
||||
}
|
||||
);
|
||||
|
||||
// Запуск шутки
|
||||
// true - отключает проверки случайности
|
||||
window.AprilJokes.start('alert', true);
|
||||
// Запуск шутки
|
||||
// true - отключает проверки случайности
|
||||
window.AprilJokes.start("alert", true);
|
||||
</script>
|
||||
<!-- Конец первоапрельских шуток: версия для разработчиков -->
|
||||
```
|
||||
|
||||
В планах:
|
||||
* Отключение отдельных шуток для пользователей.
|
||||
|
||||
О том, какие шутки определены по умолчанию, смотрите [на странице со списком их настроек](JOKES.md).
|
||||
|
||||
Если же вам интересна их реализация, то ищите её в папке [с их исходным кодом](src/jokes).
|
||||
|
||||
Обратите внимание на то, что имя скрипта здесь `april-fools.js` (в стандартном примере — `april-fools.min.js`). Эта версия скрипта обновляется чаще, а также позволяет видеть относительно читабельный код в DevTools.
|
||||
|
||||
Vendored
+209
-19
File diff suppressed because one or more lines are too long
Vendored
+7
-1
File diff suppressed because one or more lines are too long
Generated
+5348
-1678
File diff suppressed because it is too large
Load Diff
+20
-5
@@ -1,7 +1,17 @@
|
||||
{
|
||||
"version": "1.1.2",
|
||||
"name": "mybb-april-fools",
|
||||
"description": "Система первоапрельских розыгрышей для форумов MyBB",
|
||||
"homepage": "https://forumd.ru/viewtopic.php?id=7372",
|
||||
"author": {
|
||||
"name": "kozhilya",
|
||||
"url": "https://forumd.ru/viewtopic.php?id=6412"
|
||||
},
|
||||
"repository": "github:kozhilya/mybb-april-fools",
|
||||
"license": "https://forumd.ru/viewtopic.php?id=5751#rule",
|
||||
"devDependencies": {
|
||||
"@babel/cli": "^7.21.0",
|
||||
"@babel/core": "^7.21.3",
|
||||
"@babel/core": "^7.24.3",
|
||||
"@babel/preset-env": "^7.20.2",
|
||||
"@babel/preset-typescript": "^7.21.0",
|
||||
"@types/jquery": "^3.5.16",
|
||||
@@ -9,15 +19,19 @@
|
||||
"@types/node": "^18.15.10",
|
||||
"@webpack-cli/generators": "^3.0.1",
|
||||
"babelify": "^10.0.0",
|
||||
"eslint": "^8.57.0",
|
||||
"eslint-config-google": "^0.14.0",
|
||||
"grunt": "^1.6.1",
|
||||
"grunt-browserify": "^6.0.0",
|
||||
"grunt-contrib-uglify": "^5.2.2",
|
||||
"grunt-jsdoc-to-markdown": "^6.0.0",
|
||||
"html-loader": "^4.2.0",
|
||||
"jsdoc-babel": "^0.5.0",
|
||||
"jsdoc-to-markdown": "^8.0.0",
|
||||
"markdown-it": "^13.0.1",
|
||||
"markdown-it-container": "^3.0.0",
|
||||
"prettier": "^2.8.2",
|
||||
"terser-webpack-plugin": "^5.3.10",
|
||||
"ts-loader": "^9.4.2",
|
||||
"tsify": "^5.0.4",
|
||||
"typescript": "^5.0.2",
|
||||
@@ -32,10 +46,11 @@
|
||||
"watch": "webpack --watch"
|
||||
},
|
||||
"dependencies": {
|
||||
"@babel/eslint-parser": "^7.24.1",
|
||||
"@typescript-eslint/eslint-plugin": "^7.3.1",
|
||||
"cursor-effects": "^1.0.11",
|
||||
"ignore-loader": "^0.1.2",
|
||||
"jquery": "^3.6.4",
|
||||
"webpack-utf8-bom": "^1.4.0"
|
||||
},
|
||||
"version": "1.0.0",
|
||||
"description": "Mybb April Fools",
|
||||
"name": "mybb-april-fools"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,210 @@
|
||||
window.AprilJokes.disableAll();
|
||||
|
||||
window.AprilJokes.jokes.cursor.disableAll();
|
||||
|
||||
window.AprilJokes.settings = {
|
||||
core: {
|
||||
enabled: true,
|
||||
chance: 100,
|
||||
},
|
||||
|
||||
shake: {
|
||||
enabled: true,
|
||||
chance: 1,
|
||||
directions: ["top", "left", "bottom", "left"],
|
||||
force: 0.5,
|
||||
selector: "[id^=pun]",
|
||||
interval: 100,
|
||||
},
|
||||
|
||||
mirror: {
|
||||
enabled: false,
|
||||
chance: 30,
|
||||
},
|
||||
|
||||
carnival: {
|
||||
enabled: true,
|
||||
chance: 30,
|
||||
selector: 'img[src*="/avatars/"]',
|
||||
variant_chance: 20,
|
||||
variants: {
|
||||
"sepia({0}%)": 100,
|
||||
"saturate({0}%)": 100,
|
||||
"hue-rotate({0}deg)": 360,
|
||||
"invert({0}%)": 100,
|
||||
},
|
||||
},
|
||||
|
||||
humanitarian_hell: {
|
||||
enabled: true,
|
||||
chance: 20,
|
||||
selector: ".post-content",
|
||||
},
|
||||
|
||||
crazy_letters: {
|
||||
enabled: true,
|
||||
chance: 20,
|
||||
selector: ".pa-author a",
|
||||
},
|
||||
|
||||
letter_chaos: {
|
||||
enabled: true,
|
||||
chance: 20,
|
||||
selector: ".post-content",
|
||||
shuffle_chance: 50,
|
||||
},
|
||||
|
||||
cursor: {
|
||||
enabled: true,
|
||||
chance: 100,
|
||||
effects: {
|
||||
rainbow: {
|
||||
enabled: true,
|
||||
weight: 50,
|
||||
length: 20,
|
||||
size: 3,
|
||||
colors: [
|
||||
"#9400D3",
|
||||
"#4B0082",
|
||||
"#0000FF",
|
||||
"#00FF00",
|
||||
"#FFFF00",
|
||||
"#FF7F00",
|
||||
"#FF0000",
|
||||
],
|
||||
},
|
||||
springy_emoji: {
|
||||
enabled: true,
|
||||
weight: 50,
|
||||
emoji: "🤪",
|
||||
},
|
||||
emoji: {
|
||||
enabled: true,
|
||||
weight: 50,
|
||||
emoji: ["🔥", "🐬", "🦆"],
|
||||
},
|
||||
fairy_dust: {
|
||||
enabled: true,
|
||||
weight: 50,
|
||||
colors: ["#ff0000", "#00ff00", "#0000ff"],
|
||||
},
|
||||
ghost: {
|
||||
enabled: true,
|
||||
weight: 50,
|
||||
},
|
||||
trailing: {
|
||||
enabled: true,
|
||||
weight: 50,
|
||||
particles: 15,
|
||||
rate: 0.4,
|
||||
baseImageSrc: null,
|
||||
},
|
||||
text_flag: {
|
||||
enabled: true,
|
||||
weight: 50,
|
||||
text: "С днём смеха!",
|
||||
color: "#000000",
|
||||
font: "monospace",
|
||||
textSize: 12,
|
||||
gap: 14,
|
||||
},
|
||||
following_dot: {
|
||||
enabled: true,
|
||||
weight: 50,
|
||||
color: "#323232a6",
|
||||
},
|
||||
bubbles: {
|
||||
enabled: true,
|
||||
weight: 50,
|
||||
},
|
||||
snowflake: {
|
||||
enabled: false,
|
||||
weight: 50,
|
||||
},
|
||||
clock: {
|
||||
enabled: true,
|
||||
weight: 50,
|
||||
dateColor: (string = "blue"),
|
||||
faceColor: (string = "black"),
|
||||
secondsColor: (string = "red"),
|
||||
minutesColor: (string = "black"),
|
||||
hoursColor: (string = "black"),
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
stupid_title: {
|
||||
enabled: true,
|
||||
chance: 20,
|
||||
gender_resolver: null,
|
||||
rude: false,
|
||||
change_frequency: 5,
|
||||
schemas: {
|
||||
"{adj} {noun}{p}": 10,
|
||||
"{noun} {adj}{p}": 3,
|
||||
"{adj}, {adj} {noun}{p}": 10,
|
||||
"{adj} и очень {adj} {noun}{p}": 10,
|
||||
"{adj} {noun} {verb}{p}": 10,
|
||||
"{adj} {noun} {adv} {verb}{p}": 5,
|
||||
"{adj} {noun} {verb} {place}{p}": 5,
|
||||
"{adj} {noun} {adv} {verb} {place}{p}": 5,
|
||||
},
|
||||
variants: {
|
||||
adj: {
|
||||
...window.AprilJokes.settings.stupid_title.variants.adj,
|
||||
...BuildStupdiTitleVariant(true, "скромный, шутливый, !грубый"),
|
||||
...BuildStupdiTitleVariant(false, "скромная, шутливая, !грубая"),
|
||||
},
|
||||
noun: { ...window.AprilJokes.settings.stupid_title.variants.noun },
|
||||
verb: { ...window.AprilJokes.settings.stupid_title.variants.verb },
|
||||
place: { ...window.AprilJokes.settings.stupid_title.variants.place },
|
||||
adv: { ...window.AprilJokes.settings.stupid_title.variants.adv },
|
||||
p: { ...window.AprilJokes.settings.stupid_title.variants.p },
|
||||
},
|
||||
},
|
||||
|
||||
inverted_scroll: {
|
||||
enabled: true,
|
||||
chance: 10,
|
||||
},
|
||||
|
||||
latinization: {
|
||||
enabled: true,
|
||||
chance: 10,
|
||||
selector: '.post-content',
|
||||
ruToEn: {
|
||||
'а': 'a', 'б': 'b', 'в': 'v', 'г': 'g', 'д': 'd',
|
||||
'е': 'e', 'ё': 'e', 'ж': 'j', 'з': 'z', 'и': 'i',
|
||||
'й': 'j', 'к': 'k', 'л': 'l', 'м': 'm', 'н': 'n',
|
||||
'о': 'o', 'п': 'p', 'р': 'r', 'с': 's', 'т': 't',
|
||||
'у': 'u', 'ф': 'f', 'х': 'h', 'ц': 'c', 'ч': 'ch',
|
||||
'ш': 'sh', 'щ': 'shch', 'ъ': 'ie', 'ы': 'y', 'ь': '',
|
||||
'э': 'e', 'ю': 'u', 'я': 'ya',
|
||||
},
|
||||
enToRu: {
|
||||
'a': 'а', 'b': 'б', 'c': 'с', 'd': 'д', 'e': 'е',
|
||||
'f': 'ф', 'gh': 'ж', 'g': 'г', 'h': 'х', 'i': 'и',
|
||||
'j': 'й', 'k': 'к', 'l': 'л', 'm': 'м', 'n': 'н',
|
||||
'o': 'о', 'p': 'п', 'q': 'кью', 'r': 'р', 's': 'с',
|
||||
't': 'т', 'u': 'ю', 'v': 'в', 'w': 'в', 'x': 'кс',
|
||||
'y': 'ы', 'z': 'з',
|
||||
},
|
||||
},
|
||||
|
||||
fresh_grass: {
|
||||
enabled: true,
|
||||
|
||||
chance: 100,
|
||||
|
||||
keepDistance: 200,
|
||||
maxVelocity: 500,
|
||||
minVelocity: 20,
|
||||
tickPerSecond: 50,
|
||||
gravity: 20,
|
||||
correctionForce: 0.8,
|
||||
mouseForceMax: 5000,
|
||||
mouseForcePow: 5,
|
||||
mass: 1,
|
||||
collisionElasticity: 0.9,
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,573 @@
|
||||
/**
|
||||
* Отключение всех шуток
|
||||
*/
|
||||
window.AprilJokes.disableAll();
|
||||
|
||||
/**
|
||||
* Отключение всех эффектов курсора
|
||||
*/
|
||||
window.AprilJokes.jokes.cursor.disableAll();
|
||||
|
||||
window.AprilJokes.settings = {
|
||||
/**
|
||||
* Глобальные настройки
|
||||
*/
|
||||
core: {
|
||||
/**
|
||||
* Активность системы.
|
||||
* Установите это значение `false`, что бы отключить всю системы.
|
||||
* Если `true`, то система будет обработана как обычно.
|
||||
*/
|
||||
enabled: true,
|
||||
|
||||
/**
|
||||
* Вероятность срабатывания всей системы в процентах.
|
||||
* Если установлено значение `100`, система будет срабатывать всегда.
|
||||
* Если установлено значение `0`, система никогда не сработает.
|
||||
* Лучше отключать систему, используя `enabled = false`!
|
||||
*/
|
||||
chance: 100,
|
||||
},
|
||||
|
||||
/**
|
||||
* Форумо-трясение
|
||||
*
|
||||
* Страница форума начинает трястись.
|
||||
*
|
||||
* @author Kozhilya
|
||||
*/
|
||||
shake: {
|
||||
enabled: false,
|
||||
chance: 1,
|
||||
|
||||
/**
|
||||
* Направление тряски
|
||||
*/
|
||||
directions: ["top", "left", "bottom", "left"],
|
||||
|
||||
/**
|
||||
* Сила тряски
|
||||
*/
|
||||
force: 0.5,
|
||||
|
||||
/**
|
||||
* Селектор всех элементов, которые будут трястись
|
||||
*/
|
||||
selector: "[id^=pun]",
|
||||
|
||||
/**
|
||||
* Частота тряски
|
||||
*/
|
||||
interval: 100,
|
||||
},
|
||||
|
||||
/**
|
||||
* Зеркало
|
||||
*
|
||||
* Элементы форума отражаются (по крайней мере, пытаются)
|
||||
*
|
||||
* @author Kozhilya
|
||||
*/
|
||||
mirror: {
|
||||
enabled: false,
|
||||
chance: 30,
|
||||
},
|
||||
|
||||
/**
|
||||
* Карнавал
|
||||
*
|
||||
* Аватарки приобретают интересные цвета
|
||||
*
|
||||
* @author Kozhilya
|
||||
*/
|
||||
carnival: {
|
||||
enabled: true,
|
||||
chance: 30,
|
||||
|
||||
/**
|
||||
* Селектор всех элементов, которые будут перекрашены.
|
||||
*/
|
||||
selector: 'img[src*="/avatars/"]',
|
||||
|
||||
/**
|
||||
* Вероятность применения фильтра (может сработать нескольк раз).
|
||||
*/
|
||||
variant_chance: 20,
|
||||
|
||||
/**
|
||||
* Список фильтров и диапозона случайных параметров.
|
||||
*/
|
||||
variants: {
|
||||
"sepia({0}%)": 100,
|
||||
"saturate({0}%)": 100,
|
||||
"hue-rotate({0}deg)": 360,
|
||||
"invert({0}%)": 100,
|
||||
},
|
||||
},
|
||||
|
||||
/**
|
||||
* Ад гуманитария
|
||||
*
|
||||
* Все "тся" заменяются на "ться", а все "ться" на "тся"
|
||||
*
|
||||
* @author Kozhilya
|
||||
*/
|
||||
humanitarian_hell: {
|
||||
enabled: true,
|
||||
chance: 20,
|
||||
|
||||
/**
|
||||
* Селектор всех элементов, которые будут затронуты.
|
||||
*/
|
||||
selector: ".post-content",
|
||||
},
|
||||
|
||||
/**
|
||||
* Мама, Я сОшЛа С уМа!
|
||||
*
|
||||
* иМеНа ЗаБоРчИкОм
|
||||
*
|
||||
* @author Kozhilya
|
||||
*/
|
||||
crazy_letters: {
|
||||
enabled: true,
|
||||
chance: 20,
|
||||
|
||||
/**
|
||||
* Селектор всех элементов, которые будут затронуты.
|
||||
*/
|
||||
selector: ".pa-author a",
|
||||
},
|
||||
|
||||
/**
|
||||
* **Буквенный хаос**
|
||||
*
|
||||
* Все буквы в словах, кроме первой и последней, перемешены.
|
||||
*
|
||||
* @author Kozhilya
|
||||
*/
|
||||
letter_chaos: {
|
||||
enabled: true,
|
||||
chance: 20,
|
||||
|
||||
/**
|
||||
* Селектор всех элементов, которые будут затронуты
|
||||
*/
|
||||
selector: ".post-content",
|
||||
|
||||
/**
|
||||
* Вероятность того, что буквы будут перемешаны
|
||||
*/
|
||||
shuffle_chance: 50,
|
||||
},
|
||||
|
||||
/**
|
||||
* **Красивый курсор**
|
||||
*
|
||||
* Эффекты, следующие за курсором
|
||||
*
|
||||
* @author Tim Holman
|
||||
* @author Kozhilya
|
||||
*
|
||||
* @link https://tholman.com/cursor-effects/
|
||||
*/
|
||||
cursor: {
|
||||
enabled: true,
|
||||
chance: 100,
|
||||
|
||||
/**
|
||||
* Список эффектов курсора
|
||||
*/
|
||||
effects: {
|
||||
/**
|
||||
* Радужный след за курсором
|
||||
*/
|
||||
rainbow: {
|
||||
enabled: true,
|
||||
weight: 50,
|
||||
|
||||
/**
|
||||
* Длина полосы
|
||||
*/
|
||||
length: 20,
|
||||
|
||||
/**
|
||||
* Ширина полосы
|
||||
*/
|
||||
size: 3,
|
||||
|
||||
/**
|
||||
* Цвета полос (сверху вниз)
|
||||
*/
|
||||
colors: [
|
||||
"#9400D3",
|
||||
"#4B0082",
|
||||
"#0000FF",
|
||||
"#00FF00",
|
||||
"#FFFF00",
|
||||
"#FF7F00",
|
||||
"#FF0000",
|
||||
],
|
||||
},
|
||||
|
||||
/**
|
||||
* Цепочка из эмодзи "на резинке" под курсором
|
||||
*/
|
||||
springy_emoji: {
|
||||
enabled: true,
|
||||
weight: 50,
|
||||
|
||||
/**
|
||||
* Висящий эмодзи
|
||||
*/
|
||||
emoji: "🤪",
|
||||
},
|
||||
|
||||
/**
|
||||
* Падающие от курсора эмодзи
|
||||
*/
|
||||
emoji: {
|
||||
enabled: true,
|
||||
weight: 50,
|
||||
|
||||
/**
|
||||
* Падающие эмодзи
|
||||
*/
|
||||
emoji: ["🔥", "🐬", "🦆"],
|
||||
},
|
||||
|
||||
/**
|
||||
* Падающие от курсора пылинки
|
||||
*/
|
||||
fairy_dust: {
|
||||
enabled: true,
|
||||
weight: 50,
|
||||
|
||||
/**
|
||||
* Цвет пылинок
|
||||
*/
|
||||
colors: ["#ff0000", "#00ff00", "#0000ff"],
|
||||
},
|
||||
|
||||
/**
|
||||
* След "зависших" курсоров
|
||||
*/
|
||||
ghost: {
|
||||
enabled: true,
|
||||
weight: 50,
|
||||
},
|
||||
|
||||
/**
|
||||
* След курсоров "на резинке"
|
||||
*/
|
||||
trailing: {
|
||||
enabled: true,
|
||||
weight: 50,
|
||||
|
||||
/**
|
||||
* Количество элементов
|
||||
*/
|
||||
particles: 15,
|
||||
|
||||
/**
|
||||
* Скорость частиц
|
||||
*/
|
||||
rate: 0.4,
|
||||
|
||||
/**
|
||||
* Ссылка на картинку с курсором
|
||||
* Используйте `null`, что бы использовать картинку курсора по умолчанию
|
||||
*/
|
||||
baseImageSrc: null,
|
||||
},
|
||||
|
||||
/**
|
||||
* Текст, развивающийся флагом от курсора
|
||||
*/
|
||||
text_flag: {
|
||||
enabled: true,
|
||||
weight: 50,
|
||||
|
||||
/**
|
||||
* Отображаемый текст
|
||||
*/
|
||||
text: "С днём смеха!",
|
||||
|
||||
/**
|
||||
* Цвет текста
|
||||
*/
|
||||
color: "#000000",
|
||||
|
||||
/**
|
||||
* Шрифт текста
|
||||
*/
|
||||
font: "monospace",
|
||||
|
||||
/**
|
||||
* Размер текста
|
||||
*/
|
||||
textSize: 12,
|
||||
|
||||
/**
|
||||
* Растояние между бквами
|
||||
*/
|
||||
gap: 14,
|
||||
},
|
||||
|
||||
/**
|
||||
* Точка, преследующая курсор
|
||||
*/
|
||||
following_dot: {
|
||||
enabled: true,
|
||||
weight: 50,
|
||||
|
||||
/**
|
||||
* Цвет догоняющей точки
|
||||
*/
|
||||
color: "#323232a6",
|
||||
},
|
||||
|
||||
/**
|
||||
* Пузырьки от курсора
|
||||
*/
|
||||
bubbles: {
|
||||
enabled: true,
|
||||
weight: 50,
|
||||
},
|
||||
|
||||
/**
|
||||
* Снежинки, летящие от курсора
|
||||
*
|
||||
* Отключено, потому что сейчас не зима :D
|
||||
*/
|
||||
snowflake: {
|
||||
enabled: false,
|
||||
weight: 50,
|
||||
},
|
||||
|
||||
/**
|
||||
* Циферблат вокруг курсора
|
||||
*/
|
||||
clock: {
|
||||
enabled: true,
|
||||
weight: 50,
|
||||
|
||||
/**
|
||||
* Цвет даты
|
||||
*/
|
||||
dateColor: (string = "blue"),
|
||||
|
||||
/**
|
||||
* Цвет циферблата
|
||||
*/
|
||||
faceColor: (string = "black"),
|
||||
|
||||
/**
|
||||
* Цвет секундной стрелки
|
||||
*/
|
||||
secondsColor: (string = "red"),
|
||||
|
||||
/**
|
||||
* Цвет минутной стрелки
|
||||
*/
|
||||
minutesColor: (string = "black"),
|
||||
|
||||
/**
|
||||
* Цвет часовой стрелки
|
||||
*/
|
||||
hoursColor: (string = "black"),
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
/**
|
||||
* **Глупые статусы**
|
||||
*
|
||||
* Каждому пользователю даётся новый глупый статус.
|
||||
*
|
||||
* @author Kozhilya
|
||||
*/
|
||||
stupid_title: {
|
||||
enabled: true,
|
||||
chance: 20,
|
||||
|
||||
/**
|
||||
* Определение пола по имени.
|
||||
* По умолчанию род устанавливается случайным образом
|
||||
*
|
||||
* Если установлено значение `null`, система попытается загрузить пол, установленный в настройках аккаунта. Если этот пол определить невозможно, то он будет выбран случайно. Эта настройка позволяет изменить это поведение, если ваш форум обладает ролевой системой, в которой пол определяется иным образом.
|
||||
*
|
||||
* @param name Имя персонажа
|
||||
* @returns true, если мужской род; false, если женский род
|
||||
*/
|
||||
gender_resolver: null,
|
||||
|
||||
/**
|
||||
* Разрешить возможно оскорбительные слова
|
||||
*/
|
||||
rude: false,
|
||||
|
||||
/**
|
||||
* Частота смены статусов (в минутах)
|
||||
* Установите значение 0, что бы обновление происходило при каждом обновлении страницы
|
||||
*/
|
||||
change_frequency: 5,
|
||||
|
||||
/**
|
||||
* Разрешённые схемы формирования статусов
|
||||
*
|
||||
* {adj} - прилагательное
|
||||
* {noun} - имя существительно
|
||||
* {verb} - глагол
|
||||
* {adv} - наречие
|
||||
* {p} - знак препинания
|
||||
*/
|
||||
schemas: {
|
||||
"{adj} {noun}{p}": 10,
|
||||
"{noun} {adj}{p}": 3,
|
||||
"{adj}, {adj} {noun}{p}": 10,
|
||||
"{adj} и очень {adj} {noun}{p}": 10,
|
||||
"{adj} {noun} {verb}{p}": 10,
|
||||
"{adj} {noun} {adv} {verb}{p}": 5,
|
||||
"{adj} {noun} {verb} {place}{p}": 5,
|
||||
"{adj} {noun} {adv} {verb} {place}{p}": 5,
|
||||
},
|
||||
|
||||
/**
|
||||
* Вариант слов.
|
||||
* Смотрите раздел описания скрипта, что бы настроить это поле
|
||||
*/
|
||||
variants: {
|
||||
adj: {
|
||||
...window.AprilJokes.settings.stupid_title.variants.adj,
|
||||
...BuildStupdiTitleVariant(true, "скромный, шутливый, !грубый"),
|
||||
...BuildStupdiTitleVariant(false, "скромная, шутливая, !грубая"),
|
||||
},
|
||||
noun: { ...window.AprilJokes.settings.stupid_title.variants.noun },
|
||||
verb: { ...window.AprilJokes.settings.stupid_title.variants.verb },
|
||||
place: { ...window.AprilJokes.settings.stupid_title.variants.place },
|
||||
adv: { ...window.AprilJokes.settings.stupid_title.variants.adv },
|
||||
p: { ...window.AprilJokes.settings.stupid_title.variants.p },
|
||||
},
|
||||
},
|
||||
|
||||
/**
|
||||
* **Обратная гравитация**
|
||||
*
|
||||
* Скролл в обратную сторону (крутишь вверх, а крутится вниз)
|
||||
*
|
||||
* @author Идея: Alex_63
|
||||
* @author Реализация: Kozhilya
|
||||
*/
|
||||
inverted_scroll: {
|
||||
enabled: true,
|
||||
chance: 10,
|
||||
},
|
||||
|
||||
/**
|
||||
* **Латинизация**
|
||||
*
|
||||
* Все символы на странице или только в постах или еще где-то местами -
|
||||
* латиницей вместо кириллицы и наоборот
|
||||
*
|
||||
* @author Идея: Alex_63
|
||||
* @author Kozhilya
|
||||
*/
|
||||
latinization: {
|
||||
enabled: true,
|
||||
chance: 10,
|
||||
|
||||
/**
|
||||
* Селектор, в котором может изменяться текст
|
||||
*/
|
||||
selector: '.post-content',
|
||||
|
||||
/**
|
||||
* Правила преобразования из кириллицы в латиницу
|
||||
*/
|
||||
ruToEn: {
|
||||
'а': 'a', 'б': 'b', 'в': 'v', 'г': 'g', 'д': 'd',
|
||||
'е': 'e', 'ё': 'e', 'ж': 'j', 'з': 'z', 'и': 'i',
|
||||
'й': 'j', 'к': 'k', 'л': 'l', 'м': 'm', 'н': 'n',
|
||||
'о': 'o', 'п': 'p', 'р': 'r', 'с': 's', 'т': 't',
|
||||
'у': 'u', 'ф': 'f', 'х': 'h', 'ц': 'c', 'ч': 'ch',
|
||||
'ш': 'sh', 'щ': 'shch', 'ъ': 'ie', 'ы': 'y', 'ь': '',
|
||||
'э': 'e', 'ю': 'u', 'я': 'ya',
|
||||
},
|
||||
|
||||
/**
|
||||
* Правила преобразования из латиницы в кириллицу
|
||||
*/
|
||||
enToRu: {
|
||||
'a': 'а', 'b': 'б', 'c': 'с', 'd': 'д', 'e': 'е',
|
||||
'f': 'ф', 'gh': 'ж', 'g': 'г', 'h': 'х', 'i': 'и',
|
||||
'j': 'й', 'k': 'к', 'l': 'л', 'm': 'м', 'n': 'н',
|
||||
'o': 'о', 'p': 'п', 'q': 'кью', 'r': 'р', 's': 'с',
|
||||
't': 'т', 'u': 'ю', 'v': 'в', 'w': 'в', 'x': 'кс',
|
||||
'y': 'ы', 'z': 'з',
|
||||
},
|
||||
},
|
||||
|
||||
/**
|
||||
* **Свежая трава**
|
||||
*
|
||||
* Предложение потрогать траву
|
||||
* @author Идея: Magia
|
||||
* @author Kozhilya
|
||||
*/
|
||||
fresh_grass: {
|
||||
enabled: true,
|
||||
|
||||
chance: 100,
|
||||
|
||||
/**
|
||||
* Расстояние, на котором трава начнёт убегать
|
||||
*/
|
||||
keepDistance: 200,
|
||||
|
||||
/**
|
||||
* Максимальная скорость картинки (в пикселях в секунду)
|
||||
*/
|
||||
maxVelocity: 500,
|
||||
|
||||
/**
|
||||
* Минимальная скорость картинки (в пикселях в секунду)
|
||||
*/
|
||||
minVelocity: 20,
|
||||
|
||||
/**
|
||||
* Количество отметок времени за секунду
|
||||
*/
|
||||
tickPerSecond: 50,
|
||||
|
||||
/**
|
||||
* Сила притяжения в центр
|
||||
*/
|
||||
gravity: 20,
|
||||
|
||||
/**
|
||||
* Сила исправления скорости
|
||||
*/
|
||||
correctionForce: 0.8,
|
||||
|
||||
/**
|
||||
* Максимальная сила отталкивания от курсора
|
||||
*/
|
||||
mouseForceMax: 5000,
|
||||
|
||||
/**
|
||||
* Степень увеличения силы отталкивания от курсора
|
||||
*/
|
||||
mouseForcePow: 5,
|
||||
|
||||
/**
|
||||
* Общая масса картинки
|
||||
*/
|
||||
mass: 1,
|
||||
|
||||
/**
|
||||
* Доля скорости, сохраняемой при столкновении со стеной
|
||||
*/
|
||||
collisionElasticity: 0.9,
|
||||
}
|
||||
};
|
||||
+106
@@ -0,0 +1,106 @@
|
||||
import $ from 'jquery';
|
||||
|
||||
/**
|
||||
* Получение всех элементов, являющимися TextNode.
|
||||
* Используется некоторыми шутками для уменьшения
|
||||
* влияния на существующее DOM-дерево.
|
||||
*
|
||||
* @param {HTMLElement | JQuery<HTMLElement> | JQuery.Selector} root
|
||||
* Элемет, у которого необходимо найти текстовые ноды
|
||||
*
|
||||
* @return {JQuery<HTMLElement | Text | Comment | Document>}
|
||||
*/
|
||||
export function getTextNodes(
|
||||
root: HTMLElement | JQuery<HTMLElement> | JQuery.Selector,
|
||||
)
|
||||
: JQuery<HTMLElement | Text | Comment | Document> {
|
||||
// Это очень странный код, но надо успокоить ESLint
|
||||
const $root = (typeof root === 'string') ? $(root) : $(root);
|
||||
|
||||
return $root.
|
||||
find('*').
|
||||
contents().
|
||||
filter((_, node) => node.nodeType === Node.TEXT_NODE);
|
||||
}
|
||||
|
||||
export let debugEnabled = ((scriptElement) => {
|
||||
if (!(scriptElement instanceof HTMLScriptElement)) {
|
||||
return false;
|
||||
}
|
||||
return !/\.min\.js$/.test(scriptElement.src);
|
||||
})(document.currentScript);
|
||||
|
||||
/**
|
||||
* Отоброзить отладочное сообщение
|
||||
*/
|
||||
export function debug(...data: (string | number | object)[]) {
|
||||
if (!debugEnabled) return;
|
||||
console.log(...data);
|
||||
}
|
||||
|
||||
/**
|
||||
* Включить отладку
|
||||
*/
|
||||
export function enableDebug() {
|
||||
debugEnabled = true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Проверка сенсорного экрана
|
||||
* @return {boolean}
|
||||
*/
|
||||
export function isTouchEnabled(): boolean {
|
||||
return 'ontouchstart' in window ||
|
||||
navigator.maxTouchPoints > 0 ||
|
||||
// eslint-disable-next-line
|
||||
((navigator as any).msMaxTouchPoints ?? 0) > 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Проверка, является ли переменная числом
|
||||
* @param {any} n Проверяемая переменная
|
||||
* @return {boolean} Результат проверки
|
||||
*/
|
||||
// eslint-disable-next-line
|
||||
export function isNumber(n: any): boolean {
|
||||
return Number(n) === n;
|
||||
}
|
||||
|
||||
/**
|
||||
* Проверка, является ли переменная целым числом
|
||||
* @param {any} n Проверяемая переменная
|
||||
* @return {boolean} Результат проверки
|
||||
*/
|
||||
// eslint-disable-next-line
|
||||
export function isInt(n: any): boolean {
|
||||
return Number(n) === n && n % 1 === 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Проверка, является ли переменная вещественным числом
|
||||
* @param {any} n Проверяемая переменная
|
||||
* @return {boolean} Результат проверки
|
||||
*/
|
||||
// eslint-disable-next-line
|
||||
export function isFloat(n: any): boolean {
|
||||
return Number(n) === n && n % 1 !== 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Тег литеральной функции, в которой вещественные числа будут
|
||||
* печататься с 2 точками после запятой
|
||||
* @param {TemplateStringsArray} strings
|
||||
* @param {any[]} values
|
||||
* @return {string}
|
||||
*/
|
||||
// eslint-disable-next-line
|
||||
export function d2(strings: TemplateStringsArray, ...values: any[]): string {
|
||||
let result = '';
|
||||
for (let i = 0; i < strings.length; i++) {
|
||||
result += strings[i];
|
||||
if (i < values.length) {
|
||||
result += isFloat(values[i]) ? values[i].toFixed(2) : values[i];
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
import {
|
||||
StupidTitleVariants,
|
||||
BuildStupidTitleVariant,
|
||||
} from '../jokes/stupid_title';
|
||||
|
||||
/**
|
||||
* Метод, генерирующий стандартный список слов
|
||||
* @param {StupidTitleVariants} vars
|
||||
*/
|
||||
export function StupidTitleDefaultVariants(vars: StupidTitleVariants) {
|
||||
vars.adj = {
|
||||
...BuildStupidTitleVariant(
|
||||
true,
|
||||
// eslint-disable-next-line max-len
|
||||
'адекватный,$адский,активный,алчный,%ангельский,банальный,безнадёжный,$бесполезный,былой,верный,виртуальный,вкусный,воспитанный,выгодный,выходной,гигантский,$глупый,гордый,грамотный,громадный,грубый,далекий,двуличный,деятельный,доброжелательный,$жалкий,жёсткий,жестокий,$жирный,заинтересованный,запасной,застенчивый,знакомый,иностранный,истинный,конструктивный,креативный,ласковый,любимый,мелочный,^мирный,мстительный,небесный,невозможный,независимый,низкий,нужный,объективный,обыкновенный,опрятный,^оригинальный,особенный,острый,откровенный,открытый,пестрый,повседневный,подземный,пожилой,показательный,последний,посторонний,провинциальный,пышный,робкий,родной,самовлюблённый,северный,седой,семейный,серьёзный,сногсшибательный,^солнечный,спокойный,суровый,терпеливый,уверенный в себе,ударный,^удачливый,формальный,хладнокровный,целый,чистый,чрезвычайный,чужой,южный'
|
||||
),
|
||||
...BuildStupidTitleVariant(
|
||||
false,
|
||||
// eslint-disable-next-line max-len
|
||||
'адекватная,$адская,активная,алчная,%ангельская,банальная,безнадёжная,$бесполезная,былая,верная,виртуальная,вкусная,воспитанная,выгодная,выходная,гигантская,$глупая,гордая,грамотная,громадная,грубая,далекая,двуличная,деятельная,доброжелательная,$жалкая,жёсткая,жестокая,$жирная,заинтересованная,запасная,застенчивая,знакомая,иностранная,истинная,конструктивная,креативная,ласковая,любимая,мелочная,^мирная,мстительная,небесная,невозможная,независимая,низкая,нужная,объективная,обыкновенная,опрятная,^оригинальная,особенная,острая,откровенная,открытая,пестрая,повседневная,подземная,пожилая,показательная,последняя,посторонняя,провинциальная,пышная,робкая,родная,самовлюблённая,северная,седая,семейная,серьезная,сногсшибательная,^солнечная,спокаяная,суровая,терпеливая,уверенная в себе,ударная,^удачливая,формальная,хладнокровная,целая,чистая,чрезвычайная,чужая,южная'
|
||||
),
|
||||
...vars.adj,
|
||||
};
|
||||
vars.noun = {
|
||||
...BuildStupidTitleVariant(
|
||||
true,
|
||||
// eslint-disable-next-line max-len
|
||||
'актёр,$балбес,бард,басурманин,борец,бродяга,бруталище,волк,дворф,%дед,%дедушка,джентльмен,диван,динозавр,дракон,ёж,%ёжик,жук,зайц,$злоумышленник,зомби,карасик,кентавр,кит,кот,кролик,кулак,лев,ледник,лесоруб,%мальчег,мальчик,манекен,массажист,мачо,монах,монстр,наследник,ниндзя,носорог,одиночка,организм,орёл,отец,принц,проказник,пузырь,$раб,рыцарь,спортсмен,сурикат,сын,%сынок,тигр,товарищ,хоббит,эльф'
|
||||
),
|
||||
...BuildStupidTitleVariant(
|
||||
false,
|
||||
// eslint-disable-next-line max-len
|
||||
'актриса,акула,$бабка,бабочка,%бабушка,бактерия,белочка,бусинка,валькирия,$ведьма,волторна,волчица,горгулья,дворфиха,девочка,деревяшка,детка,%деффачка,%дочка,дочь,заноза,$злоумышленница,знаменитость,каруселька,килька,конфетка,косточка,кошка,красотка,крышка,леди,$личинка,львица,мать,машина,милашка,монашка,мышь,наследница,орлица,панда,пиранья,принцесса,простуда,$рабыня,русалочка,рыбка,рыбка,скамеечка,спортсменка,тигрица,травка,черепашка,чертовка,эльфийка'
|
||||
),
|
||||
...vars.noun,
|
||||
};
|
||||
vars.verb = {
|
||||
...BuildStupidTitleVariant(
|
||||
null,
|
||||
// eslint-disable-next-line max-len
|
||||
'бежит,берёт и делает,болеет,встаёт,говорит,думает,забывает,замирает,заходит,идёт,%имеет план и придерживается его,ловит,меняется,молчит,нарушает,оглядывается,орёт,освобождает,отвечает,отказаться куда-то идти,отказывается,отказывается вставать,отказывается что-то делать,оформляет,плывёт,поглядывает,поздравляет,поздравляет,получает,прибывает,приказывает,просыпается,прощает,работает,радуется,скрывается,снимает,снимает,снимается в сериале,снимается в фильме,собирается,соединяет,спрашивает,тянет,указывает,учит,читает,что-то скрывает,чувствует опасность,шепчет'
|
||||
),
|
||||
...BuildStupidTitleVariant(
|
||||
true,
|
||||
// eslint-disable-next-line max-len
|
||||
'взял и сделал,додумался,женился,женится,заработался,не спал,не успел,обиделся,познал дзен,присел,проснулся,скрылся,смог,успел'
|
||||
),
|
||||
...BuildStupidTitleVariant(
|
||||
false,
|
||||
// eslint-disable-next-line max-len
|
||||
'взяла и сделала,выходит замуж,вышла замуж,додумалась,заработалась,не спала,не успела,обиделась,познала дзен,присела,проснулась,скрылась,смогла,успела'
|
||||
),
|
||||
...vars.verb,
|
||||
};
|
||||
vars.place = {
|
||||
...BuildStupidTitleVariant(
|
||||
null,
|
||||
// eslint-disable-next-line max-len
|
||||
'в джунглях,в квартире,в космосе,в лесу,в очереди,в пустыне,в театре,%в тёплой и уютной квартире,в филармонии,где-то,на заснеженной вершине,на конференции,на пляже,на форуме,под водой,под дождём,%под проливным дождём'
|
||||
),
|
||||
...vars.place,
|
||||
};
|
||||
vars.adv = {
|
||||
...BuildStupidTitleVariant(
|
||||
null,
|
||||
// eslint-disable-next-line max-len
|
||||
'безрассудно,быстро,внезапно,вновь,$грубо,грустно,медленно,%молниеносно,немедленно,опять,осмысленно,позже,предсказуемо,расчётливо,регулярно,счастливо,уже,уныло'
|
||||
),
|
||||
...vars.adv,
|
||||
};
|
||||
}
|
||||
-139
@@ -1,139 +0,0 @@
|
||||
import { Joke, JokeSettings } from "./joke";
|
||||
|
||||
/**
|
||||
* Блок обработки перво-апрельских шуток
|
||||
*
|
||||
* @author Kozhilya
|
||||
*/
|
||||
export class AprilFoolsJokeClass {
|
||||
/**
|
||||
* Проверка вероятности
|
||||
*
|
||||
* @param {number} chance Вероятность срабатывания
|
||||
* @returns {boolean} Флаг срабатывания (true - сработало)
|
||||
*/
|
||||
static check(chance: number): boolean {
|
||||
return Math.random() < 0.01 * chance;
|
||||
}
|
||||
|
||||
/**
|
||||
* Добавление шутки
|
||||
*
|
||||
* @param joke
|
||||
*/
|
||||
add(joke: Joke): void {
|
||||
this.jokes[joke.id] = joke;
|
||||
}
|
||||
|
||||
addCustom(id: string, handler: Function, settings: JokeSettings|null = null) {
|
||||
const joke = new class extends Joke {
|
||||
id = id;
|
||||
|
||||
settings: JokeSettings = { chance: 10 };
|
||||
|
||||
start() {
|
||||
handler.call(this, this.settings);
|
||||
}
|
||||
};
|
||||
|
||||
Object.assign(joke.settings, settings ?? {});
|
||||
|
||||
this.add(joke);
|
||||
}
|
||||
|
||||
core_settings = new CoreSettings;
|
||||
|
||||
get settings(): SettingsMap {
|
||||
const result: SettingsMap = {};
|
||||
|
||||
result['core'] = this.core_settings;
|
||||
|
||||
for (const entries of Object.entries(this.jokes)) {
|
||||
const id = entries[0];
|
||||
const joke = entries[1];
|
||||
|
||||
result[id] = joke.settings;
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Запись новых настроек
|
||||
*
|
||||
* @param settings
|
||||
*/
|
||||
set settings(settings: SettingsMap) {
|
||||
for (const entry of Object.entries(settings)) {
|
||||
const id = entry[0];
|
||||
const jokeSettings = entry[1];
|
||||
const targetSettings = (id === 'core') ? this.core_settings : this.jokes[id].settings;
|
||||
|
||||
Object.assign(targetSettings, jokeSettings);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Запуск шутки
|
||||
*
|
||||
* @param id
|
||||
* @param {boolean} forced
|
||||
*/
|
||||
start(id: string, forced: boolean = false) {
|
||||
if (!(id in this.jokes))
|
||||
return;
|
||||
|
||||
const joke = this.jokes[id];
|
||||
|
||||
if (!forced && !AprilFoolsJokeClass.check(joke.settings.chance)) {
|
||||
return;
|
||||
}
|
||||
|
||||
joke.start();
|
||||
}
|
||||
|
||||
/**
|
||||
* Запуск всех шуток
|
||||
*/
|
||||
startAll(forced: boolean = false): void {
|
||||
for (const key of Object.keys(this.jokes)) {
|
||||
this.start(key, forced);
|
||||
}
|
||||
}
|
||||
|
||||
regularStart(): void {
|
||||
const today = new Date();
|
||||
const month = today.getMonth() + 1; // Note that getMonth() returns 0-indexed month, so we need to add 1
|
||||
const day = today.getDate();
|
||||
|
||||
if (month === 4 && day === 1 && AprilFoolsJokeClass.check(this.core_settings.chance)) {
|
||||
this.startAll();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Перечисление всех шуток
|
||||
*/
|
||||
jokes: JokeMap = {}
|
||||
}
|
||||
|
||||
/**
|
||||
* Объект хранения шуток
|
||||
*/
|
||||
interface JokeMap {
|
||||
[key: string]: Joke;
|
||||
}
|
||||
|
||||
/**
|
||||
* Объект задания настроек
|
||||
*/
|
||||
interface SettingsMap {
|
||||
[key: string]: JokeSettings;
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
class CoreSettings implements JokeSettings {
|
||||
chance = 100;
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
<button class="april-fools__button">🎉</button>
|
||||
@@ -0,0 +1,16 @@
|
||||
<div class="april-fools__joke__fresh-grass">
|
||||
<div class="april-fools__joke__fresh-grass__window">
|
||||
<h4>А ты трогал сегодня траву?</h4>
|
||||
|
||||
<img class="april-fools__joke__fresh-grass__image"
|
||||
src="https://upload.wikimedia.org/wikipedia/commons/thumb/1/13/Green_Grass.JPG/1280px-Green_Grass.JPG"
|
||||
alt="Здесь должная быть трава, но что-то сломалось... Притворись, что она здесь есть, хорошо?">
|
||||
|
||||
<small>
|
||||
Изображение травы взято с
|
||||
<a href="https://commons.wikimedia.org/wiki/File:Green_Grass.JPG">WikiMedia</a>
|
||||
под лицензией
|
||||
<a href="https://creativecommons.org/licenses/by-sa/4.0/">CC BY-SA 4.0 DEED</a>.
|
||||
</small>
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,129 @@
|
||||
<style class="april-fools__styles">
|
||||
.april-fools__button {
|
||||
position: fixed;
|
||||
bottom: 0.2rem;
|
||||
right: 0.2rem;
|
||||
padding: 0;
|
||||
font-size: 3rem;
|
||||
border: 0 solid;
|
||||
background: transparent none;
|
||||
opacity: 0.3;
|
||||
transition: 0.4s all;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.april-fools__button:hover {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.april-fools__joke-row {
|
||||
border-bottom: 1px solid black;
|
||||
}
|
||||
|
||||
.april-fools__joke-row:first-of-type {
|
||||
border-top: 1px solid black;
|
||||
}
|
||||
|
||||
.april-fools__joke {
|
||||
display: flex;
|
||||
flex-flow: row wrap;
|
||||
padding: 10px;
|
||||
opacity: 0.5;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
gap: 1ch;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.april-fools__joke:hover {
|
||||
text-shadow: 0 0 black;
|
||||
}
|
||||
|
||||
.april-fools__joke:before {
|
||||
display: block;
|
||||
flex: 0 0 2ch;
|
||||
text-align: center;
|
||||
content: "✔";
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
input.april-fools__joke-input:checked + .april-fools__joke {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
input.april-fools__joke-input:checked + .april-fools__joke:before {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.april-fools__joke .april-fools__joke-title {
|
||||
flex: 1 auto;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.april-fools__joke .april-fools__joke-description {
|
||||
flex: 1 auto;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.april-fools__joke .april-fools__joke-chance {
|
||||
flex: 0 0 4ch;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.april-fools__content .april-fools__reset-button {
|
||||
display: block;
|
||||
margin: 10px auto;
|
||||
}
|
||||
|
||||
/* Шутка "Свежая трава" */
|
||||
html:has(.april-fools__joke__fresh-grass),
|
||||
body:has(.april-fools__joke__fresh-grass) {
|
||||
position: fixed;
|
||||
width: 100%;
|
||||
left: 0;
|
||||
top: var(--fresh-grass-body-top);
|
||||
}
|
||||
|
||||
.april-fools__joke__fresh-grass {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background-color: #008000C0;
|
||||
z-index: 1000;
|
||||
}
|
||||
|
||||
.april-fools__joke__fresh-grass__window {
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
display: flex;
|
||||
flex-flow: column nowrap;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.april-fools__joke__fresh-grass__window h4 {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
color: #FFFFFF;
|
||||
margin: 1vh 0;
|
||||
font-size: 2vh;
|
||||
line-height: 2vh;
|
||||
text-align: center;
|
||||
}
|
||||
.april-fools__joke__fresh-grass__window small {
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
}
|
||||
|
||||
.april-fools__joke__fresh-grass__window img {
|
||||
display: block;
|
||||
max-width: 10vw;
|
||||
max-height: 10vh;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,23 @@
|
||||
<div class="april-fools__content">
|
||||
<div class="april-fools__jokes"></div>
|
||||
|
||||
<button class="april-fools__reset-button">
|
||||
Вернуться к настройкам форума (страница будет перезагружена!)
|
||||
</button>
|
||||
</div>
|
||||
<template class="april-fools__template" id="april-fools__window-row">
|
||||
<div class="april-fools__joke-row">
|
||||
<input
|
||||
type="checkbox"
|
||||
class="april-fools__joke-input"
|
||||
name="april-fools__joke-${joke.id}"
|
||||
id="april-fools__joke-${joke.id}"
|
||||
style="display: none"
|
||||
/>
|
||||
<label class="april-fools__joke" for="april-fools__joke-${joke.id}">
|
||||
<span class="april-fools__joke-title">${joke.title}</span>
|
||||
<span class="april-fools__joke-description">${joke.description}</span>
|
||||
<span class="april-fools__joke-chance">${joke.settings.chance}%</span>
|
||||
</label>
|
||||
</div>
|
||||
</template>
|
||||
+103
-28
@@ -1,45 +1,120 @@
|
||||
/**
|
||||
* Объект настроек шутки
|
||||
*/
|
||||
export interface JokeSettings {
|
||||
/**
|
||||
* Вероятность срабатывания в процентах
|
||||
*/
|
||||
chance: number;
|
||||
export class JokeSettings {
|
||||
/**
|
||||
* Флаг блокировки срабатывания
|
||||
*/
|
||||
enabled: boolean = true;
|
||||
|
||||
/**
|
||||
* Вероятность срабатывания в процентах
|
||||
*/
|
||||
chance: number = 20;
|
||||
}
|
||||
|
||||
/**
|
||||
* Объект, описывающий шутку
|
||||
*/
|
||||
export abstract class Joke {
|
||||
/**
|
||||
* Идентификатор шутки
|
||||
*/
|
||||
abstract id: string;
|
||||
export abstract class Joke<T extends JokeSettings> {
|
||||
/**
|
||||
* Идентификатор шутки
|
||||
*/
|
||||
abstract id: string;
|
||||
|
||||
/**
|
||||
* Настройки шутки
|
||||
*/
|
||||
settings: JokeSettings;
|
||||
/**
|
||||
* Заголовок шутки
|
||||
*/
|
||||
abstract title: string;
|
||||
|
||||
/**
|
||||
* Флаг, указывающий, должна ли шутка быть запущена
|
||||
*/
|
||||
enabled: boolean = true;
|
||||
/**
|
||||
* Описание шутки
|
||||
*/
|
||||
abstract description: string;
|
||||
|
||||
/**
|
||||
* Запуск шутки
|
||||
*/
|
||||
abstract start(): void;
|
||||
/**
|
||||
* Настройки шутки
|
||||
*/
|
||||
_settings: T;
|
||||
|
||||
/**
|
||||
* Получение полного списка настроек
|
||||
*/
|
||||
get settings(): T {
|
||||
return this._settings;
|
||||
}
|
||||
|
||||
/**
|
||||
* Запись новых настроек
|
||||
*
|
||||
* @param {JokeSettings} settings
|
||||
*/
|
||||
set settings(settings: T) {
|
||||
for (const entry of Object.entries(settings)) {
|
||||
const id = entry[0];
|
||||
const value = entry[1];
|
||||
|
||||
switch (id) {
|
||||
case 'name':
|
||||
case 'title':
|
||||
this.title = value;
|
||||
break;
|
||||
|
||||
case 'description':
|
||||
this.description = value;
|
||||
break;
|
||||
|
||||
default:
|
||||
// eslint-disable-next-line
|
||||
(this._settings as any)[id] = value;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Проверка вероятности
|
||||
*
|
||||
* @param {number} chance Вероятность срабатывания
|
||||
* @return {boolean} Флаг срабатывания (true - сработало)
|
||||
*/
|
||||
check(chance: number): boolean {
|
||||
return Math.random() < 0.01 * chance;
|
||||
}
|
||||
|
||||
/**
|
||||
* Разблокировать шутку
|
||||
*/
|
||||
enable() {
|
||||
this.settings.enabled = true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Заблокировать шутку
|
||||
*/
|
||||
disable() {
|
||||
this.settings.enabled = false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Переключить блокировку шутки
|
||||
* @param {boolean | null} [value] Новое значение
|
||||
*/
|
||||
toggle(value: boolean | null = null) {
|
||||
this.settings.enabled = value === null ? !this.settings.enabled : value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Запуск шутки
|
||||
*/
|
||||
abstract start(): void;
|
||||
}
|
||||
|
||||
/**
|
||||
* Объект, описывающий шутку, которая может быть остановлена
|
||||
*/
|
||||
export abstract class StopableJoke extends Joke
|
||||
{
|
||||
/**
|
||||
* Остановка шутки, если возможно
|
||||
*/
|
||||
abstract stop(): void;
|
||||
export abstract class StoppableJoke<T extends JokeSettings> extends Joke<T> {
|
||||
/**
|
||||
* Остановка шутки, если возможно
|
||||
*/
|
||||
abstract stop(): void;
|
||||
}
|
||||
+324
@@ -0,0 +1,324 @@
|
||||
import './types/mybb.d';
|
||||
import {Joke, JokeSettings, StoppableJoke} from './joke';
|
||||
import {JokerUI} from './ui';
|
||||
import {debug} from './common';
|
||||
|
||||
/**
|
||||
* Блок обработки перво-апрельских шуток
|
||||
*
|
||||
* @author Kozhilya
|
||||
*/
|
||||
export class JokerClass {
|
||||
ui: JokerUI = new JokerUI(this);
|
||||
/**
|
||||
* Общие настройки системы
|
||||
*/
|
||||
core_settings = new CoreSettings();
|
||||
|
||||
/**
|
||||
* Перечисление всех шуток
|
||||
*/
|
||||
jokes: JokeMap = {};
|
||||
|
||||
/**
|
||||
* Получение полного списка настроек
|
||||
*/
|
||||
get settings(): SettingsMap {
|
||||
const result: SettingsMap = {};
|
||||
|
||||
result['core'] = this.core_settings;
|
||||
|
||||
for (const entries of Object.entries(this.jokes)) {
|
||||
const id = entries[0];
|
||||
const joke = entries[1];
|
||||
|
||||
result[id] = joke.settings;
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Запись новых настроек
|
||||
*
|
||||
* @param {SettingsMap} settings
|
||||
*/
|
||||
set settings(settings: SettingsMap) {
|
||||
for (const entry of Object.entries(settings)) {
|
||||
const id = entry[0];
|
||||
const jokeSettings = entry[1];
|
||||
const targetSettings =
|
||||
id === 'core' ? this.core_settings : this.jokes[id].settings;
|
||||
|
||||
Object.assign(targetSettings, jokeSettings);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Проверка вероятности
|
||||
*
|
||||
* @param {number} chance Вероятность срабатывания
|
||||
* @return {boolean} Флаг срабатывания (true - сработало)
|
||||
*/
|
||||
check(chance: number): boolean {
|
||||
return Math.random() < 0.01 * chance;
|
||||
}
|
||||
|
||||
/**
|
||||
* Добавление шутки
|
||||
*
|
||||
* @param {Joke} joke Объект шутки
|
||||
*/
|
||||
add(joke: Joke<JokeSettings>): void {
|
||||
this.jokes[joke.id] = joke;
|
||||
}
|
||||
|
||||
/**
|
||||
* Регистрация новой шутки
|
||||
*
|
||||
* @param {string} id Идентификатор шутки
|
||||
* @param {JokeHandler} handler Метод, выполняющий шутку
|
||||
* @param {JokeSettings | null} [settings] Объект настроек шутки
|
||||
* @return {Joke<JokeSettings>} Объект шутки
|
||||
*/
|
||||
addCustom(
|
||||
id: string,
|
||||
handler: JokeHandler,
|
||||
settings: JokeSettings | null = null,
|
||||
): Joke<JokeSettings> {
|
||||
const joke = new (class extends Joke<JokeSettings> {
|
||||
id = id;
|
||||
|
||||
title = 'Пользовательская шутка';
|
||||
|
||||
description = 'Для шутки не определено описание';
|
||||
|
||||
_settings = new JokeSettings();
|
||||
|
||||
/**
|
||||
* Метод запуска шутки
|
||||
*/
|
||||
start() {
|
||||
handler.call(this, this.settings);
|
||||
}
|
||||
})();
|
||||
|
||||
joke.settings = settings;
|
||||
|
||||
this.add(joke);
|
||||
|
||||
return joke;
|
||||
}
|
||||
|
||||
/**
|
||||
* Разблокировать систему
|
||||
*/
|
||||
enable() {
|
||||
this.core_settings.enabled = true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Заблокировать систему
|
||||
*/
|
||||
disable() {
|
||||
this.core_settings.enabled = false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Разблокировать все шутки
|
||||
*/
|
||||
enableAll() {
|
||||
for (const key of Object.keys(this.jokes)) {
|
||||
this.jokes[key].enable();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Заблокировать все шутки
|
||||
*/
|
||||
disableAll() {
|
||||
for (const key of Object.keys(this.jokes)) {
|
||||
this.jokes[key].disable();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Переключение шутки
|
||||
*
|
||||
* @param {string} jokeId ID шутки для переключения
|
||||
* @param {boolean} state Новое состояние
|
||||
*/
|
||||
toggleJoke(jokeId: string, state: boolean) {
|
||||
const joke = this.jokes[jokeId];
|
||||
joke.settings.enabled = state;
|
||||
|
||||
if (joke instanceof StoppableJoke && !state) {
|
||||
joke.stop();
|
||||
}
|
||||
|
||||
this.saveUserStates();
|
||||
}
|
||||
|
||||
/**
|
||||
* Сохранение пользовательских настроек
|
||||
*/
|
||||
saveUserStates() {
|
||||
const states: { [key in string]: boolean } = {};
|
||||
|
||||
for (const key of Object.keys(this.jokes)) {
|
||||
states[key] = this.jokes[key].settings.enabled;
|
||||
}
|
||||
|
||||
fetch(
|
||||
'/api.php?' +
|
||||
$.param({
|
||||
method: 'storage.set',
|
||||
user_id: window.UserID,
|
||||
key: 'april-fools',
|
||||
token: window.ForumAPITicket,
|
||||
value: JSON.stringify(states),
|
||||
timer: 24 * 60,
|
||||
}),
|
||||
).then((_) => {});
|
||||
}
|
||||
|
||||
/**
|
||||
* Загрузка пользовательских настроек
|
||||
*/
|
||||
async loadUserStates() {
|
||||
const response = await fetch(
|
||||
'/api.php?' +
|
||||
$.param({
|
||||
method: 'storage.get',
|
||||
user_id: window.UserID,
|
||||
key: 'april-fools',
|
||||
}),
|
||||
);
|
||||
const json = await response.json();
|
||||
|
||||
if (json.error) {
|
||||
return;
|
||||
}
|
||||
|
||||
const data = JSON.parse(json.response.storage.data['april-fools']);
|
||||
|
||||
for (const key of Object.keys(this.jokes)) {
|
||||
this.jokes[key].settings.enabled = data[key];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Отчистка настроек пользователя
|
||||
*/
|
||||
async clearUserStates() {
|
||||
await fetch(
|
||||
'/api.php?' +
|
||||
$.param({
|
||||
method: 'storage.delete',
|
||||
user_id: window.UserID,
|
||||
key: 'april-fools',
|
||||
token: window.ForumAPITicket,
|
||||
}),
|
||||
);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
/**
|
||||
* Запуск шутки
|
||||
*
|
||||
* @param {string} jokeId ID шутки для запуска
|
||||
* @param {boolean} [forced] Принудительный запуск
|
||||
*/
|
||||
start(jokeId: string, forced: boolean = false) {
|
||||
if (!(jokeId in this.jokes)) return;
|
||||
|
||||
const joke = this.jokes[jokeId];
|
||||
|
||||
if (!forced && (!joke.settings.enabled || !this.check(joke.settings.chance))
|
||||
) {
|
||||
debug(joke.title, '- шутка не включена');
|
||||
return;
|
||||
}
|
||||
debug(joke.title, '- шутка включена');
|
||||
joke.start();
|
||||
}
|
||||
|
||||
/**
|
||||
* Запуск всех шуток
|
||||
*
|
||||
* @param {boolean} [forced] Принудительный запуск
|
||||
*/
|
||||
startAll(forced: boolean = false): void {
|
||||
for (const key of Object.keys(this.jokes)) {
|
||||
this.start(key, forced);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Стандартная проверка 1 апреля
|
||||
*
|
||||
* @return {boolean} Флаг, должна ли запускаться система
|
||||
*/
|
||||
regularCheck(): boolean {
|
||||
if (!this.core_settings.enabled) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (this.core_settings.testers.indexOf(window.UserID) >= 0) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const today = new Date();
|
||||
const month = today.getMonth() + 1;
|
||||
const day = today.getDate();
|
||||
|
||||
if (month !== 4 || day !== 1) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return this.check(this.core_settings.chance);
|
||||
}
|
||||
|
||||
/**
|
||||
* Стандартный запуск 1 апреля
|
||||
*/
|
||||
async regularStart() {
|
||||
if (!this.regularCheck()) {
|
||||
return;
|
||||
}
|
||||
|
||||
await this.loadUserStates();
|
||||
this.ui.show();
|
||||
this.startAll();
|
||||
}
|
||||
}
|
||||
|
||||
type JokeHandler = () => void;
|
||||
|
||||
/**
|
||||
* Объект хранения шуток
|
||||
*/
|
||||
interface JokeMap {
|
||||
[key: string]: Joke<JokeSettings>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Объект задания настроек
|
||||
*/
|
||||
interface SettingsMap {
|
||||
[key: string]: JokeSettings;
|
||||
}
|
||||
|
||||
/**
|
||||
* Общие правила системы
|
||||
*/
|
||||
class CoreSettings implements JokeSettings {
|
||||
chance = 100;
|
||||
enabled = true;
|
||||
|
||||
/**
|
||||
* Список пользователей-тестировщиков
|
||||
*/
|
||||
testers: number[] = [];
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
import {Joke, JokeSettings} from '../joke';
|
||||
import $ from 'jquery';
|
||||
|
||||
/**
|
||||
* **Шаблон**
|
||||
*
|
||||
* Описание
|
||||
*
|
||||
* @author Kozhilya
|
||||
*/
|
||||
export class TemplateJoke extends Joke<TemplateJokeSettings> {
|
||||
id = '';
|
||||
|
||||
title = '';
|
||||
|
||||
description = '';
|
||||
|
||||
_settings = new TemplateJokeSettings();
|
||||
|
||||
/**
|
||||
* Запуск шутки
|
||||
*/
|
||||
start(): void {
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Класс настроек для шутки ""
|
||||
*/
|
||||
export class TemplateJokeSettings implements JokeSettings {
|
||||
enabled: boolean = true;
|
||||
|
||||
chance: number = 100;
|
||||
}
|
||||
+55
-39
@@ -1,6 +1,5 @@
|
||||
import { Joke, JokeSettings, StopableJoke } from "../joke";
|
||||
import $ from "jquery";
|
||||
import { AprilFoolsJokeClass } from "../handler";
|
||||
import {Joke, JokeSettings} from '../joke';
|
||||
import $ from 'jquery';
|
||||
|
||||
/**
|
||||
* **Карнавал**
|
||||
@@ -9,54 +8,71 @@ import { AprilFoolsJokeClass } from "../handler";
|
||||
*
|
||||
* @author Kozhilya
|
||||
*/
|
||||
export class CarnivalJoke extends Joke {
|
||||
id = 'carnival'
|
||||
export class CarnivalJoke extends Joke<CarnivalJokeSettings> {
|
||||
id = 'carnival';
|
||||
|
||||
settings = new CarnivalJokeSettings;
|
||||
title = 'Карнавал';
|
||||
|
||||
start(): void {
|
||||
$(this.settings.selector).each((_, elem) => {
|
||||
const filters: string[] = [];
|
||||
description = 'Аватарки приобретают интересные цвета';
|
||||
|
||||
for (const entry of Object.entries(this.settings.variants)) {
|
||||
if (!AprilFoolsJokeClass.check(this.settings.variant_chance)) {
|
||||
continue;
|
||||
}
|
||||
_settings = new CarnivalJokeSettings();
|
||||
|
||||
filters.push(entry[0].replace('{0}', Math.round(Math.random() * entry[1]).toString()));
|
||||
}
|
||||
/**
|
||||
* Запуск шутки
|
||||
*/
|
||||
start(): void {
|
||||
$(this.settings.selector).each((_, elem) => {
|
||||
const filters: string[] = [];
|
||||
|
||||
if (filters.length > 0) {
|
||||
$(elem).css({'filter': filters.join(' ')});
|
||||
}
|
||||
});
|
||||
}
|
||||
for (const entry of Object.entries(this.settings.variants)) {
|
||||
if (!this.check(this.settings.variant_chance)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
filters.push(
|
||||
entry[0].replace(
|
||||
'{0}',
|
||||
Math.round(Math.random() * entry[1]).toString()
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
if (filters.length > 0) {
|
||||
$(elem).css({filter: filters.join(' ')});
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Класс настроек для шутки "Карнавал"
|
||||
*/
|
||||
export class CarnivalJokeSettings implements JokeSettings {
|
||||
chance: number = 30;
|
||||
enabled: boolean = true;
|
||||
|
||||
/**
|
||||
* Селектор всех элементов, которые будут перекрашены
|
||||
*/
|
||||
selector: string = 'img[src*="/avatars/"]';
|
||||
chance: number = 30;
|
||||
|
||||
/**
|
||||
* Вероятность применения некоторого фильтра
|
||||
*/
|
||||
variant_chance: number = 20;
|
||||
/**
|
||||
* Селектор всех элементов, которые будут перекрашены
|
||||
*/
|
||||
selector: string = 'img[src*="/avatars/"]';
|
||||
|
||||
/**
|
||||
* Список фильтров и диапозона случайных параметров
|
||||
*/
|
||||
variants: CarnivalJokeFilters = {
|
||||
'sepia({0}%)': 100,
|
||||
'saturate({0}%)': 100,
|
||||
'hue-rotate({0}deg)': 360,
|
||||
'invert({0}%)': 100,
|
||||
}
|
||||
/**
|
||||
* Вероятность применения некоторого фильтра
|
||||
*/
|
||||
variant_chance: number = 20;
|
||||
|
||||
/**
|
||||
* Список фильтров и диапозона случайных параметров
|
||||
*/
|
||||
variants: CarnivalJokeFilters = {
|
||||
'sepia({0}%)': 100,
|
||||
'saturate({0}%)': 100,
|
||||
'hue-rotate({0}deg)': 360,
|
||||
'invert({0}%)': 100,
|
||||
};
|
||||
}
|
||||
|
||||
export interface CarnivalJokeFilters {
|
||||
[key: string]: number;
|
||||
[key: string]: number;
|
||||
}
|
||||
+32
-20
@@ -1,5 +1,5 @@
|
||||
import { Joke, JokeSettings, StopableJoke } from "../joke";
|
||||
import $ from "jquery";
|
||||
import {Joke, JokeSettings} from '../joke';
|
||||
import $ from 'jquery';
|
||||
|
||||
/**
|
||||
* **Мама, Я сОшЛа С уМа!**
|
||||
@@ -8,30 +8,42 @@ import $ from "jquery";
|
||||
*
|
||||
* @author Kozhilya
|
||||
*/
|
||||
export class CrazyLettersJoke extends Joke {
|
||||
id = 'crazy_letters';
|
||||
export class CrazyLettersJoke extends Joke<CrazyLettersJokeSettings> {
|
||||
id = 'crazy_letters';
|
||||
|
||||
settings = new CrazyLettersJokeSettings;
|
||||
title = 'Мама, Я сОшЛа С уМа!';
|
||||
|
||||
start(): void {
|
||||
$(this.settings.selector).each((_, elem) => {
|
||||
let text = $(this).text();
|
||||
let result = '';
|
||||
description = 'иМеНа ЗаБоРчИкОм';
|
||||
|
||||
for (let i = 0; i < text.length; i++) {
|
||||
result += text[i][(i % 2) ? 'toLowerCase' : 'toUpperCase']();
|
||||
}
|
||||
_settings = new CrazyLettersJokeSettings();
|
||||
|
||||
$(this).text(result);
|
||||
});
|
||||
}
|
||||
/**
|
||||
* Запуск шутки
|
||||
*/
|
||||
start(): void {
|
||||
$(this.settings.selector).each((_, elem) => {
|
||||
const text = $(elem).text();
|
||||
let result = '';
|
||||
|
||||
for (let i = 0; i < text.length; i++) {
|
||||
result += text[i][i % 2 ? 'toLowerCase' : 'toUpperCase']();
|
||||
}
|
||||
|
||||
$(this).text(result);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Класс настроек для шутки "Мама, Я сОшЛа С уМа!"
|
||||
*/
|
||||
export class CrazyLettersJokeSettings implements JokeSettings {
|
||||
chance: number = 20;
|
||||
enabled: boolean = true;
|
||||
|
||||
/**
|
||||
* Селектор всех элементов, которые будут затронуты
|
||||
*/
|
||||
selector: string = '.pa-author a';
|
||||
chance: number = 20;
|
||||
|
||||
/**
|
||||
* Селектор всех элементов, которые будут затронуты
|
||||
*/
|
||||
selector: string = '.pa-author a';
|
||||
}
|
||||
@@ -1,60 +0,0 @@
|
||||
import { Joke, JokeSettings, StopableJoke } from "../joke";
|
||||
import $ from "jquery";
|
||||
|
||||
/**
|
||||
* **Бешеные запятые**
|
||||
*
|
||||
* Запятые после слов
|
||||
*
|
||||
* @author Kozhilya
|
||||
*/
|
||||
export class CrazyPunctuationJoke extends Joke {
|
||||
id = 'crazy_punctuation';
|
||||
|
||||
settings = new CrazyPunctuationJokeSettings;
|
||||
|
||||
private transform(text: string): string {
|
||||
let newText = "";
|
||||
|
||||
for (let i = 0; i < text.length; i++) {
|
||||
const currentChar = text[i];
|
||||
const nextChar = text[i + 1];
|
||||
|
||||
if (this.settings.symbols.includes(currentChar)) {
|
||||
newText += currentChar;
|
||||
} else if (nextChar === " " || this.settings.symbols.includes(nextChar)) {
|
||||
newText += currentChar;
|
||||
}
|
||||
}
|
||||
|
||||
return newText;
|
||||
}
|
||||
|
||||
private process(node: any): void {
|
||||
|
||||
}
|
||||
|
||||
start(): void {
|
||||
const items = $(this.settings.selector).find('*').contents().filter((_, node) => node.nodeType === Node.TEXT_NODE);
|
||||
|
||||
items.each((_, node) => {
|
||||
console.log(node);
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
export class CrazyPunctuationJokeSettings implements JokeSettings {
|
||||
chance: number = 20;
|
||||
|
||||
/**
|
||||
* Селектор всех элементов, которые будут затронуты
|
||||
*/
|
||||
selector: string = '.post-content';
|
||||
|
||||
/**
|
||||
* Мигрирующие знаки
|
||||
*/
|
||||
symbols: string = ',.;:?!-()[]{}\'"';
|
||||
|
||||
|
||||
}
|
||||
@@ -0,0 +1,370 @@
|
||||
|
||||
import {JokeSettings, StoppableJoke} from '../joke';
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
||||
import * as CursorEffectsLib from 'cursor-effects';
|
||||
|
||||
/**
|
||||
* **Красивый курсор**
|
||||
*
|
||||
* Эффекты, следующие за курсором
|
||||
*
|
||||
* @author Tim Holman
|
||||
* @link https://tholman.com/cursor-effects/
|
||||
*/
|
||||
export class CursorEffectsJoke extends
|
||||
StoppableJoke<CursorEffectsJokeSettings> {
|
||||
id = 'cursor_effects';
|
||||
|
||||
title = 'Красивый курсор';
|
||||
|
||||
description = 'Эффекты, следующие за курсором';
|
||||
|
||||
_settings = new CursorEffectsJokeSettings();
|
||||
|
||||
active_cursor_effect: CursorEffectsLib.CursorEffectResult;
|
||||
|
||||
/**
|
||||
* Разблокировать все варианты курсоров
|
||||
*/
|
||||
enableAll() {
|
||||
for (const key of Object.keys(this.settings.effects)) {
|
||||
this.settings.effects[<CursorEffectsEnum>key].enabled = true;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Заблокировать все варианты курсоров
|
||||
*/
|
||||
disableAll() {
|
||||
for (const key of Object.keys(this.settings.effects)) {
|
||||
this.settings.effects[<CursorEffectsEnum>key].enabled = false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Получение веса отдельного эффекта
|
||||
* @param {CursorEffectsEnum} effect Тип эффекта
|
||||
* @return {number} Значение веса
|
||||
* @private
|
||||
*/
|
||||
private weight(effect: CursorEffectsEnum): number {
|
||||
const s = this.settings.effects[effect];
|
||||
return s.enabled ? s.weight : 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Получение случайного эффекта
|
||||
* @return {CursorEffectsEnum}
|
||||
*/
|
||||
getRandomEffect(): CursorEffectsEnum {
|
||||
const elements = Object.values(CursorEffectsEnum).map(
|
||||
(e) => <CursorEffectsEnum>e
|
||||
);
|
||||
const totalWeight = elements.reduce(
|
||||
(sum, element) => sum + this.weight(element),
|
||||
0
|
||||
);
|
||||
const randomWeight = Math.random() * totalWeight;
|
||||
|
||||
let currentWeight = 0;
|
||||
for (const element of elements) {
|
||||
currentWeight += this.weight(element);
|
||||
if (randomWeight <= currentWeight) {
|
||||
return element;
|
||||
}
|
||||
}
|
||||
|
||||
throw new Error('Тебя быть тут не должно...');
|
||||
}
|
||||
|
||||
/**
|
||||
* Запуск шутки
|
||||
*/
|
||||
start(): void {
|
||||
const effect = this.getRandomEffect();
|
||||
const settings = this.settings.effects[effect];
|
||||
|
||||
this.active_cursor_effect = resolveCursorEffectsEnum(effect, settings);
|
||||
}
|
||||
|
||||
/**
|
||||
* Остановка шутки
|
||||
*/
|
||||
stop(): void {
|
||||
this.active_cursor_effect.destroy();
|
||||
}
|
||||
}
|
||||
|
||||
enum CursorEffectsEnum {
|
||||
Rainbow = 'rainbow',
|
||||
FairyDust = 'fairy_dust',
|
||||
SpringyEmoji = 'springy_emoji',
|
||||
Emoji = 'emoji',
|
||||
Ghost = 'ghost',
|
||||
Trailing = 'trailing',
|
||||
TextFlag = 'text_flag',
|
||||
FollowingDot = 'following_dot',
|
||||
Bubbles = 'bubbles',
|
||||
Snowflake = 'snowflake',
|
||||
Clock = 'clock',
|
||||
}
|
||||
|
||||
/**
|
||||
* Получение эффекта
|
||||
* @param {CursorEffectsEnum | string} effect Тип эффекта
|
||||
* @param {CursorEffectsSetting} options Настройки эффекта
|
||||
* @return {CursorEffectsLib.CursorEffectResult}
|
||||
*/
|
||||
function resolveCursorEffectsEnum(
|
||||
effect: CursorEffectsEnum | string,
|
||||
options: CursorEffectsSetting
|
||||
): CursorEffectsLib.CursorEffectResult {
|
||||
switch (effect) {
|
||||
case CursorEffectsEnum.Rainbow:
|
||||
return CursorEffectsLib.rainbowCursor(options);
|
||||
|
||||
case CursorEffectsEnum.FairyDust:
|
||||
return CursorEffectsLib.fairyDustCursor(options);
|
||||
|
||||
case CursorEffectsEnum.SpringyEmoji:
|
||||
return CursorEffectsLib.springyEmojiCursor(options);
|
||||
|
||||
case CursorEffectsEnum.Emoji:
|
||||
return CursorEffectsLib.emojiCursor(options);
|
||||
|
||||
case CursorEffectsEnum.Ghost:
|
||||
return CursorEffectsLib.ghostCursor(options);
|
||||
|
||||
case CursorEffectsEnum.Trailing:
|
||||
return CursorEffectsLib.trailingCursor(options);
|
||||
|
||||
case CursorEffectsEnum.TextFlag:
|
||||
return CursorEffectsLib.textFlag(options);
|
||||
|
||||
case CursorEffectsEnum.FollowingDot:
|
||||
return CursorEffectsLib.followingDotCursor(options);
|
||||
|
||||
case CursorEffectsEnum.Bubbles:
|
||||
return CursorEffectsLib.bubbleCursor(options);
|
||||
|
||||
case CursorEffectsEnum.Snowflake:
|
||||
return CursorEffectsLib.snowflakeCursor(options);
|
||||
|
||||
case CursorEffectsEnum.Clock:
|
||||
return CursorEffectsLib.clockCursor(options);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Класс настроек для шутки "Красивый курсор"
|
||||
*/
|
||||
export class CursorEffectsJokeSettings implements JokeSettings {
|
||||
enabled: boolean = true;
|
||||
|
||||
chance: number = 100;
|
||||
|
||||
effects: { [key in CursorEffectsEnum]?: CursorEffectsSetting } = {
|
||||
/**
|
||||
* Радужный след за курсором
|
||||
*/
|
||||
[CursorEffectsEnum.Rainbow]:
|
||||
new (class extends CursorEffectsSetting {
|
||||
/**
|
||||
* Длина полосы
|
||||
*/
|
||||
length: number = 20;
|
||||
|
||||
/**
|
||||
* Ширина полосы
|
||||
*/
|
||||
size: number = 3;
|
||||
|
||||
/**
|
||||
* Цвета полос (сверху вниз)
|
||||
*/
|
||||
colors: string[] = [
|
||||
'#9400D3',
|
||||
'#4B0082',
|
||||
'#0000FF',
|
||||
'#00FF00',
|
||||
'#FFFF00',
|
||||
'#FF7F00',
|
||||
'#FF0000',
|
||||
];
|
||||
})(),
|
||||
|
||||
/**
|
||||
* Цепочка из эмодзи "на резинке" под курсором
|
||||
*/
|
||||
[CursorEffectsEnum.SpringyEmoji]:
|
||||
new (class extends CursorEffectsSetting {
|
||||
/**
|
||||
* Висящий эмодзи
|
||||
*/
|
||||
emoji: string = '🤪';
|
||||
})(),
|
||||
|
||||
/**
|
||||
* Падающие от курсора эмодзи
|
||||
*/
|
||||
[CursorEffectsEnum.Emoji]: new (class extends CursorEffectsSetting {
|
||||
/**
|
||||
* Падающие эмодзи
|
||||
*/
|
||||
emoji: string[] = ['🔥', '🐬', '🦆'];
|
||||
})(),
|
||||
|
||||
/**
|
||||
* Падающие от курсора пылинки
|
||||
*/
|
||||
[CursorEffectsEnum.FairyDust]:
|
||||
new (class extends CursorEffectsSetting {
|
||||
/**
|
||||
* Цвет пылинок
|
||||
*/
|
||||
colors: string[] = ['#ff0000', '#00ff00', '#0000ff'];
|
||||
})(),
|
||||
|
||||
/**
|
||||
* След "зависших" курсоров
|
||||
*/
|
||||
[CursorEffectsEnum.Ghost]: new CursorEffectsSetting(),
|
||||
|
||||
/**
|
||||
* След курсоров "на резинке"
|
||||
*/
|
||||
[CursorEffectsEnum.Trailing]:
|
||||
new (class extends CursorEffectsSetting {
|
||||
/**
|
||||
* Количество элементов
|
||||
*/
|
||||
particles: number = 15;
|
||||
|
||||
/**
|
||||
* Скорость частиц
|
||||
*/
|
||||
rate: number = 0.4;
|
||||
|
||||
/**
|
||||
* Ссылка на картинку с курсором
|
||||
* Используйте `null`, что бы использовать картинку курсора по умолчанию
|
||||
*/
|
||||
baseImageSrc: string | null = null;
|
||||
})(),
|
||||
|
||||
/**
|
||||
* Текст, развивающийся флагом от курсора
|
||||
*/
|
||||
[CursorEffectsEnum.TextFlag]:
|
||||
new (class extends CursorEffectsSetting {
|
||||
/**
|
||||
* Отображаемый текст
|
||||
*/
|
||||
text: string = 'С днём смеха!';
|
||||
|
||||
/**
|
||||
* Цвет текста
|
||||
*/
|
||||
color: string = '#000000';
|
||||
|
||||
/**
|
||||
* Шрифт текста
|
||||
*/
|
||||
font: string = 'monospace';
|
||||
|
||||
/**
|
||||
* Размер текста
|
||||
*/
|
||||
textSize: number = 12;
|
||||
|
||||
/**
|
||||
* Растояние между бквами
|
||||
*/
|
||||
gap: number = 14;
|
||||
|
||||
/**
|
||||
* Размер... чего-то.
|
||||
*/
|
||||
size: number = 3;
|
||||
})(),
|
||||
|
||||
/**
|
||||
* Точка, преследующая курсор
|
||||
*/
|
||||
[CursorEffectsEnum.FollowingDot]:
|
||||
new (class extends CursorEffectsSetting {
|
||||
/**
|
||||
* Цвет догоняющей точки
|
||||
*/
|
||||
color: string = '#323232a6';
|
||||
})(),
|
||||
|
||||
/**
|
||||
* Пузырьки от курсора
|
||||
*/
|
||||
[CursorEffectsEnum.Bubbles]: new CursorEffectsSetting(),
|
||||
|
||||
/**
|
||||
* Снежинки, летящие от курсора
|
||||
*
|
||||
* Отключено, потому что сейчас не зима :D
|
||||
*/
|
||||
[CursorEffectsEnum.Snowflake]:
|
||||
new (class extends CursorEffectsSetting {
|
||||
weight: number = 0;
|
||||
})(),
|
||||
|
||||
/**
|
||||
* Циферблат вокруг курсора
|
||||
*/
|
||||
[CursorEffectsEnum.Clock]: new (class extends CursorEffectsSetting {
|
||||
/**
|
||||
* Цвет даты
|
||||
*/
|
||||
dateColor: string = 'blue';
|
||||
|
||||
/**
|
||||
* Цвет циферблата
|
||||
*/
|
||||
faceColor: string = 'black';
|
||||
|
||||
/**
|
||||
* Цвет секундной стрелки
|
||||
*/
|
||||
secondsColor: string = 'red';
|
||||
|
||||
/**
|
||||
* Цвет минутной стрелки
|
||||
*/
|
||||
minutesColor: string = 'black';
|
||||
|
||||
/**
|
||||
* Цвет часовой стрелки
|
||||
*/
|
||||
hoursColor: string = 'black';
|
||||
})(),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Восстановление класса CursorEffectsLib.DefaultOptions
|
||||
*/
|
||||
class DefaultOptions {
|
||||
readonly element?: HTMLElement;
|
||||
}
|
||||
|
||||
/**
|
||||
* Класс настроек для курсора
|
||||
*/
|
||||
class CursorEffectsSetting extends DefaultOptions {
|
||||
/**
|
||||
* Флаг возможности срабатывания
|
||||
*/
|
||||
enabled: boolean = true;
|
||||
|
||||
/**
|
||||
* Весовой коэффициент вероятности курсора
|
||||
*/
|
||||
weight: number = 50;
|
||||
}
|
||||
@@ -0,0 +1,362 @@
|
||||
import '../types/html.d';
|
||||
import {Joke, JokeSettings} from '../joke';
|
||||
import $ from 'jquery';
|
||||
import containerHTML from '../html/joke-fresh_glass.html';
|
||||
import {Vector} from '../lib/vector';
|
||||
import {debugEnabled} from '../common';
|
||||
|
||||
/**
|
||||
* **Свежая трава**
|
||||
*
|
||||
* Предложение потрогать траву
|
||||
* @author Идея: Magia
|
||||
* @author Kozhilya
|
||||
*/
|
||||
export class FreshGrassJoke extends Joke<FreshGrassJokeSettings> {
|
||||
id = 'fresh_grass';
|
||||
|
||||
title = 'Свежая трава';
|
||||
|
||||
description = 'Предложение потрогать траву';
|
||||
|
||||
_settings = new FreshGrassJokeSettings();
|
||||
|
||||
container: JQuery<HTMLDivElement>;
|
||||
|
||||
intervalIndex: NodeJS.Timeout;
|
||||
|
||||
movement: Movement;
|
||||
|
||||
/**
|
||||
* Запуск шутки
|
||||
*/
|
||||
start(): void {
|
||||
$(() => this.init());
|
||||
}
|
||||
|
||||
/**
|
||||
* Основная загрузка шутки
|
||||
*/
|
||||
init() {
|
||||
const $body = $(document.body);
|
||||
|
||||
document.body.style.setProperty(
|
||||
'--fresh-grass-body-top',
|
||||
`-${window.scrollY}px`,
|
||||
);
|
||||
|
||||
this.container = $(containerHTML);
|
||||
this.container.hide(0);
|
||||
$body.append(this.container);
|
||||
this.container.fadeIn(500);
|
||||
|
||||
this.fleeingGrass();
|
||||
}
|
||||
|
||||
/**
|
||||
* Отладка
|
||||
* @param {string} str
|
||||
*/
|
||||
debug(str: string): void {
|
||||
if (!debugEnabled) return;
|
||||
|
||||
$('small', this.container).html(`<pre>${str}</pre>`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Анимация убегания травы от пользователя
|
||||
*/
|
||||
fleeingGrass(): void {
|
||||
this.movement = new Movement(this.settings);
|
||||
|
||||
const $img = $('img.april-fools__joke__fresh-grass__image', this.container);
|
||||
|
||||
$img.on('mousedown', () => {
|
||||
this.close();
|
||||
});
|
||||
|
||||
$(window).on('mousemove', (e) => {
|
||||
this.movement.mouse = new Vector(e.clientX - window.innerWidth / 2,
|
||||
e.clientY - window.innerHeight / 2);
|
||||
});
|
||||
|
||||
this.intervalIndex = setInterval(() => {
|
||||
const img = $img[0].getBoundingClientRect();
|
||||
this.movement.step(img);
|
||||
|
||||
// this.debug(this.movement.debugLines.join('\n'));
|
||||
|
||||
$img.css('transform', this.movement.state.position.toTransform());
|
||||
}, Math.floor(1000 * this.movement.dt));
|
||||
}
|
||||
|
||||
/**
|
||||
* Закрыть окно
|
||||
*/
|
||||
close(): void {
|
||||
clearInterval(this.intervalIndex);
|
||||
|
||||
this.container.fadeOut({
|
||||
duration: 500,
|
||||
// eslint-disable-next-line
|
||||
// @ts-ignore
|
||||
complete: () => {
|
||||
this.container.remove();
|
||||
|
||||
scrollTo(0,
|
||||
-parseInt(document.body.style.getPropertyValue(
|
||||
'--fresh-grass-body-top')));
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
type MovementData = {
|
||||
position: Vector;
|
||||
velocity: Vector;
|
||||
acceleration: Vector;
|
||||
};
|
||||
|
||||
/**
|
||||
* Класс обработки движения
|
||||
*/
|
||||
class Movement {
|
||||
settings: FreshGrassJokeSettings;
|
||||
state: MovementData;
|
||||
mouse: Vector;
|
||||
dt: number;
|
||||
last: MovementData;
|
||||
debugLines: string[];
|
||||
forces: { [key in string]: Vector };
|
||||
|
||||
/**
|
||||
* Конструктор
|
||||
* @param {FreshGrassJokeSettings} settings
|
||||
*/
|
||||
constructor(settings: FreshGrassJokeSettings) {
|
||||
this.settings = settings;
|
||||
this.state = {
|
||||
position: new Vector(),
|
||||
velocity: Vector.fromLengthAngle(Math.random() * 2 * Math.PI,
|
||||
Math.random() *
|
||||
(this.settings.maxVelocity - this.settings.minVelocity) +
|
||||
this.settings.minVelocity),
|
||||
acceleration: new Vector(),
|
||||
};
|
||||
this.mouse = new Vector();
|
||||
this.dt = 1.0 / this.settings.tickPerSecond;
|
||||
}
|
||||
|
||||
/**
|
||||
* Координата движения
|
||||
*/
|
||||
get position() {
|
||||
return this.state.position;
|
||||
}
|
||||
|
||||
/**
|
||||
* Скорость движения
|
||||
*/
|
||||
get velocity() {
|
||||
return this.state.velocity;
|
||||
}
|
||||
|
||||
/**
|
||||
* Ускорение движения
|
||||
*/
|
||||
get acceleration() {
|
||||
return this.state.acceleration;
|
||||
}
|
||||
|
||||
/**
|
||||
* Произвести вычисление
|
||||
* @param {DOMRect} img Изображение
|
||||
*/
|
||||
step(img: DOMRect) {
|
||||
this.last = this.state;
|
||||
this.debugLines = [];
|
||||
this.forces = {};
|
||||
|
||||
// Обработка сил
|
||||
this.processForces();
|
||||
this.processMovement();
|
||||
this.processCollisions(img);
|
||||
}
|
||||
|
||||
/**
|
||||
* Обработка сил
|
||||
*/
|
||||
processForces() {
|
||||
// Притяжение к центру
|
||||
this.forces.gravity = Vector.fromLengthAngle(
|
||||
this.last.position.angle + Math.PI,
|
||||
this.settings.gravity,
|
||||
);
|
||||
|
||||
// Коррекция скорости
|
||||
if (this.last.velocity.length < this.settings.minVelocity) {
|
||||
this.forces.correction = Vector.fromLengthAngle(
|
||||
this.last.velocity.angle,
|
||||
this.settings.correctionForce *
|
||||
Math.pow(this.last.velocity.length - this.settings.minVelocity, 2),
|
||||
);
|
||||
} else if (this.last.velocity.length > this.settings.maxVelocity) {
|
||||
this.forces.correction = Vector.fromLengthAngle(
|
||||
this.last.velocity.angle + Math.PI,
|
||||
this.settings.correctionForce *
|
||||
Math.pow(this.last.velocity.length - this.settings.maxVelocity, 2),
|
||||
);
|
||||
}
|
||||
|
||||
// Отталкивание от мыши
|
||||
const toMouseVector = this.mouse.add(this.last.position.reverse());
|
||||
if (toMouseVector.length < this.settings.keepDistance &&
|
||||
toMouseVector.length > 0) {
|
||||
this.forces.running = Vector.fromLengthAngle(
|
||||
toMouseVector.angle + Math.PI,
|
||||
this.settings.mouseForceMax -
|
||||
Math.pow(
|
||||
Math.abs(toMouseVector.length / this.settings.keepDistance - 1),
|
||||
this.settings.mouseForcePow),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Обработка движения
|
||||
*/
|
||||
processMovement() {
|
||||
// Ускорение
|
||||
this.debugLines.push('Forces:');
|
||||
let acceleration: Vector = new Vector();
|
||||
|
||||
for (const [name, force] of Object.entries(this.forces)) {
|
||||
acceleration = acceleration.add(force);
|
||||
this.debugLines.push(` [${name}] ${force}`);
|
||||
}
|
||||
|
||||
acceleration = acceleration.multiply(1 / this.settings.mass);
|
||||
|
||||
this.state = {
|
||||
position: this.last.position,
|
||||
velocity: this.last.velocity,
|
||||
acceleration: acceleration,
|
||||
};
|
||||
|
||||
this.state.velocity = this.velocity.add(
|
||||
this.acceleration.multiply(this.dt));
|
||||
this.state.position = this.position.add(
|
||||
this.velocity.multiply(this.dt));
|
||||
|
||||
this.debugLines.push(`acceleration: ${this.acceleration}`);
|
||||
this.debugLines.push(`velocity: ${this.velocity}`);
|
||||
this.debugLines.push(`position: ${this.position}`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Обработка столкновения с границами окна
|
||||
* @param {DOMRect} img Изображение
|
||||
*/
|
||||
processCollisions(img: DOMRect) {
|
||||
const limitX = (window.innerWidth - img.width) / 2;
|
||||
const limitY = (window.innerHeight - img.height) / 2;
|
||||
|
||||
if (this.position.x < -limitX) {
|
||||
// Left
|
||||
this.position.x = -2 * limitX - this.position.x;
|
||||
this.velocity.x = -this.velocity.x * this.settings.collisionElasticity;
|
||||
}
|
||||
|
||||
if (this.position.x > limitX) {
|
||||
// Right
|
||||
this.position.x = 2 * limitX - this.position.x;
|
||||
this.velocity.x = -this.velocity.x * this.settings.collisionElasticity;
|
||||
}
|
||||
|
||||
if (this.position.y < -limitY) {
|
||||
// Top
|
||||
this.position.y = -2 * limitY - this.position.y;
|
||||
this.velocity.y = -this.velocity.y * this.settings.collisionElasticity;
|
||||
}
|
||||
|
||||
if (this.position.y > limitY) {
|
||||
// Bottom
|
||||
this.position.y = 2 * limitY - this.position.y;
|
||||
this.velocity.y = -this.velocity.y * this.settings.collisionElasticity;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Нормально распределённая случайная величина
|
||||
* @param {number} mean
|
||||
* @param {number} stdev
|
||||
* @return {number}
|
||||
* @private
|
||||
*/
|
||||
private randomGaussian(mean: number = 0, stdev: number = 1): number {
|
||||
const u = 1 - Math.random(); // Converting [0,1) to (0,1]
|
||||
const v = Math.random();
|
||||
const z = Math.sqrt(-2.0 * Math.log(u)) * Math.cos(2.0 * Math.PI * v);
|
||||
// Transform to the desired mean and standard deviation:
|
||||
return z * stdev + mean;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Класс настроек для шутки "Свежая трава"
|
||||
*/
|
||||
export class FreshGrassJokeSettings implements JokeSettings {
|
||||
enabled: boolean = true;
|
||||
|
||||
chance: number = 100;
|
||||
|
||||
/**
|
||||
* Расстояние, на котором трава начнёт убегать
|
||||
*/
|
||||
keepDistance: number = 200;
|
||||
|
||||
/**
|
||||
* Максимальная скорость картинки (в пикселях в секунду)
|
||||
*/
|
||||
maxVelocity: number = 500;
|
||||
|
||||
/**
|
||||
* Минимальная скорость картинки (в пикселях в секунду)
|
||||
*/
|
||||
minVelocity: number = 20;
|
||||
|
||||
/**
|
||||
* Количество отметок времени за секунду
|
||||
*/
|
||||
tickPerSecond: number = 50;
|
||||
|
||||
/**
|
||||
* Сила притяжения в центр
|
||||
*/
|
||||
gravity: number = 20;
|
||||
|
||||
/**
|
||||
* Сила исправления скорости
|
||||
*/
|
||||
correctionForce: number = 0.8;
|
||||
|
||||
/**
|
||||
* Максимальная сила отталкивания от курсора
|
||||
*/
|
||||
mouseForceMax: number = 5000;
|
||||
|
||||
/**
|
||||
* Степень увеличения силы отталкивания от курсора
|
||||
*/
|
||||
mouseForcePow: number = 5;
|
||||
|
||||
/**
|
||||
* Общая масса картинки
|
||||
*/
|
||||
mass: number = 1;
|
||||
|
||||
/**
|
||||
* Доля скорости, сохраняемой при столкновении со стеной
|
||||
*/
|
||||
collisionElasticity: number = 0.9;
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Joke, JokeSettings, StopableJoke } from "../joke";
|
||||
import $ from "jquery";
|
||||
import {Joke, JokeSettings} from '../joke';
|
||||
import {getTextNodes} from '../common';
|
||||
|
||||
/**
|
||||
* **Ад гуманитария**
|
||||
@@ -8,25 +8,49 @@ import $ from "jquery";
|
||||
*
|
||||
* @author Kozhilya
|
||||
*/
|
||||
export class HumanitarianHellJoke extends Joke {
|
||||
id = 'humanitarian_hell';
|
||||
export class HumanitarianHellJoke extends Joke<HumanitarianHellJokeSettings> {
|
||||
id = 'humanitarian_hell';
|
||||
|
||||
settings = new HumanitarianHellJokeSettings;
|
||||
title = 'Ад гуманитария';
|
||||
|
||||
start(): void {
|
||||
$(this.settings.selector).each(function () {
|
||||
$(this).html($(this).html().replace(/(ть?ся)/g, function (fnd) {
|
||||
return (fnd.indexOf('ь') > 0) ? 'тся' : 'ться';
|
||||
}));
|
||||
});
|
||||
}
|
||||
description = 'Все "тся" заменяются на "ться", а все "ться" на "тся"';
|
||||
|
||||
_settings = new HumanitarianHellJokeSettings();
|
||||
|
||||
/**
|
||||
* Обработка текста
|
||||
*
|
||||
* @param {string} text Входная строка
|
||||
* @return {string} Итоговая строка
|
||||
*/
|
||||
processText(text: string): string {
|
||||
return text.replace(/(ть?ся)/g, function(fnd) {
|
||||
return fnd.indexOf('ь') > 0 ? 'тся' : 'ться';
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Запуск шутки
|
||||
*/
|
||||
start(): void {
|
||||
const textNodes = getTextNodes(this.settings.selector);
|
||||
|
||||
textNodes.each((_, elem) => {
|
||||
elem.textContent = this.processText(elem.textContent);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Класс настроек для шутки "Ад гуманитария"
|
||||
*/
|
||||
export class HumanitarianHellJokeSettings implements JokeSettings {
|
||||
chance: number = 20;
|
||||
enabled: boolean = true;
|
||||
|
||||
/**
|
||||
* Селектор всех элементов, которые будут затронуты
|
||||
*/
|
||||
selector: string = '.post-content';
|
||||
chance: number = 20;
|
||||
|
||||
/**
|
||||
* Селектор всех элементов, которые будут затронуты
|
||||
*/
|
||||
selector: HTMLElement | JQuery<HTMLElement> | string = '.post-content';
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
import {Joke, JokeSettings} from '../joke';
|
||||
|
||||
/**
|
||||
* **Обратная гравитация**
|
||||
*
|
||||
* Скролл в обратную сторону (крутишь вверх, а крутится вниз)
|
||||
*
|
||||
* @author Идея: Alex_63
|
||||
* @author Реализация: Kozhilya
|
||||
*/
|
||||
export class InvertedScrollJoke extends Joke<InvertedScrollJokeSettings> {
|
||||
id = 'inverted_scroll';
|
||||
|
||||
title = 'Обратная гравитация';
|
||||
|
||||
description = 'Скролл в обратную сторону';
|
||||
|
||||
_settings = new InvertedScrollJokeSettings();
|
||||
|
||||
/**
|
||||
* Запуск шутки
|
||||
*/
|
||||
start(): void {
|
||||
// let lastScroll = window.scrollY;
|
||||
|
||||
document.addEventListener('wheel', (e) => {
|
||||
e.preventDefault();
|
||||
scrollBy(0, -e.deltaY);
|
||||
}, {passive: false});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Класс настроек для шутки "Обратная гравитация"
|
||||
*/
|
||||
export class InvertedScrollJokeSettings implements JokeSettings {
|
||||
enabled: boolean = true;
|
||||
|
||||
chance: number = 10;
|
||||
}
|
||||
@@ -0,0 +1,106 @@
|
||||
import {Joke, JokeSettings} from '../joke';
|
||||
import {getTextNodes} from '../common';
|
||||
|
||||
/**
|
||||
* **Латинизация**
|
||||
*
|
||||
* Все символы на странице или только в постах или еще где-то местами -
|
||||
* латиницей вместо кириллицы и наоборот
|
||||
*
|
||||
* @author Идея: Alex_63
|
||||
* @author Kozhilya
|
||||
*/
|
||||
export class LatinizationJoke extends Joke<LatinizationJokeSettings> {
|
||||
id = 'latinization';
|
||||
|
||||
title = 'Латинизация';
|
||||
|
||||
description = 'Все символы на странице или только в постах ' +
|
||||
'или еще где-то местами - латиницей вместо кириллицы и наоборот';
|
||||
|
||||
_settings = new LatinizationJokeSettings();
|
||||
|
||||
/**
|
||||
* Запуск шутки
|
||||
*/
|
||||
start(): void {
|
||||
const textNodes = getTextNodes(this.settings.selector);
|
||||
|
||||
textNodes.each((_, node) => this.processNode(node as Text));
|
||||
}
|
||||
|
||||
/**
|
||||
* Латинизация или кириллизация слова
|
||||
* @param {string} word
|
||||
* @return {string}
|
||||
* @private
|
||||
*/
|
||||
private latinize(word: string): string {
|
||||
if (/[а-яА-ЯёЁ0-9_-]+/.test(word)) {
|
||||
// Кириллица в латиницу
|
||||
for (const [ru, en] of Object.entries(this.settings.ruToEn)) {
|
||||
word = word.replace(new RegExp(ru, 'g'), en).
|
||||
replace(new RegExp(ru.toUpperCase(), 'gi'), en.toUpperCase());
|
||||
}
|
||||
} else if (/[a-zA-Z0-9_-]+/.test(word)) {
|
||||
// Латиница в кириллицу
|
||||
for (const [en, ru] of Object.entries(this.settings.enToRu)) {
|
||||
word = word.replace(new RegExp(en, 'g'), ru).
|
||||
replace(new RegExp(en.toUpperCase(), 'gi'), ru.toUpperCase());
|
||||
}
|
||||
}
|
||||
|
||||
return word;
|
||||
}
|
||||
|
||||
/**
|
||||
* Обработка текстовой ноды
|
||||
* @param {Text} node
|
||||
* @private
|
||||
*/
|
||||
private processNode(node: Text) {
|
||||
node.textContent = node.textContent?.replace(
|
||||
/([А-Яа-яA-Za-z]+)/gm,
|
||||
(_: string, word: string) => this.latinize(word),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Класс настроек для шутки "Латинизация"
|
||||
*/
|
||||
export class LatinizationJokeSettings implements JokeSettings {
|
||||
enabled: boolean = true;
|
||||
|
||||
chance: number = 10;
|
||||
|
||||
/**
|
||||
* Селектор, в котором может изменяться текст
|
||||
*/
|
||||
selector: string = '.post-content';
|
||||
|
||||
/**
|
||||
* Правила преобразования из кириллицы в латиницу
|
||||
*/
|
||||
ruToEn: { [key in string]: string } = {
|
||||
'а': 'a', 'б': 'b', 'в': 'v', 'г': 'g', 'д': 'd',
|
||||
'е': 'e', 'ё': 'e', 'ж': 'j', 'з': 'z', 'и': 'i',
|
||||
'й': 'j', 'к': 'k', 'л': 'l', 'м': 'm', 'н': 'n',
|
||||
'о': 'o', 'п': 'p', 'р': 'r', 'с': 's', 'т': 't',
|
||||
'у': 'u', 'ф': 'f', 'х': 'h', 'ц': 'c', 'ч': 'ch',
|
||||
'ш': 'sh', 'щ': 'shch', 'ъ': 'ie', 'ы': 'y', 'ь': '',
|
||||
'э': 'e', 'ю': 'u', 'я': 'ya',
|
||||
};
|
||||
|
||||
/**
|
||||
* Правила преобразования из латиницы в кириллицу
|
||||
*/
|
||||
enToRu: { [key in string]: string } = {
|
||||
'a': 'а', 'b': 'б', 'c': 'с', 'd': 'д', 'e': 'е',
|
||||
'f': 'ф', 'gh': 'ж', 'g': 'г', 'h': 'х', 'i': 'и',
|
||||
'j': 'й', 'k': 'к', 'l': 'л', 'm': 'м', 'n': 'н',
|
||||
'o': 'о', 'p': 'п', 'q': 'кью', 'r': 'р', 's': 'с',
|
||||
't': 'т', 'u': 'ю', 'v': 'в', 'w': 'в', 'x': 'кс',
|
||||
'y': 'ы', 'z': 'з',
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,90 @@
|
||||
import {Joke, JokeSettings} from '../joke';
|
||||
import {getTextNodes} from '../common';
|
||||
|
||||
/**
|
||||
* **Буквенный хаос**
|
||||
*
|
||||
* Все буквы в словах, кроме первой и последней, перемешены
|
||||
*
|
||||
* @author Kozhilya
|
||||
*/
|
||||
export class LetterChaosJoke extends Joke<LetterChaosJokeSettings> {
|
||||
id = 'letter_chaos';
|
||||
|
||||
title = 'Буквенный хаос';
|
||||
|
||||
description = 'Все буквы в словах, кроме первой и последней, перемешены';
|
||||
|
||||
_settings = new LetterChaosJokeSettings();
|
||||
|
||||
punctuationSymbols: string[] | null = null;
|
||||
|
||||
/**
|
||||
* Перемешивание букв в словах
|
||||
* @param {string} word
|
||||
* @return {string}
|
||||
* @private
|
||||
*/
|
||||
private shuffleLetters(word: string): string {
|
||||
if (word.length < 3 || !this.check(this.settings.shuffle_chance)) {
|
||||
return word;
|
||||
}
|
||||
|
||||
const characters = word.split('');
|
||||
|
||||
const middleCharacters = characters.slice(1, -1);
|
||||
for (let i = middleCharacters.length - 1; i > 0; i--) {
|
||||
const j = Math.floor(Math.random() * (i + 1));
|
||||
[middleCharacters[i], middleCharacters[j]] = [
|
||||
middleCharacters[j],
|
||||
middleCharacters[i],
|
||||
];
|
||||
}
|
||||
|
||||
return [
|
||||
characters[0],
|
||||
...middleCharacters,
|
||||
characters[characters.length - 1],
|
||||
].join('');
|
||||
}
|
||||
|
||||
/**
|
||||
* Обработка текстовой ноды
|
||||
* @param {Text} node
|
||||
* @private
|
||||
*/
|
||||
private processNode(node: Text) {
|
||||
node.textContent = node.textContent?.replace(
|
||||
/([А-Яа-яA-Za-z]+)/gm,
|
||||
(_: string, word: string) => this.shuffleLetters(word)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Запуск шутки
|
||||
*/
|
||||
start(): void {
|
||||
const textNodes = getTextNodes(this.settings.selector);
|
||||
|
||||
textNodes.each((_, node) => this.processNode(node as Text));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Класс настроек для шутки "Буквенный хаос"
|
||||
*/
|
||||
export class LetterChaosJokeSettings implements JokeSettings {
|
||||
enabled: boolean = true;
|
||||
|
||||
chance: number = 20;
|
||||
|
||||
/**
|
||||
* Селектор всех элементов, которые будут затронуты
|
||||
*/
|
||||
selector: string = '.post-content';
|
||||
|
||||
/**
|
||||
* Вероятность того, что буквы будут перемешаны
|
||||
*/
|
||||
shuffle_chance: number = 50;
|
||||
}
|
||||
+28
-12
@@ -1,5 +1,5 @@
|
||||
import { Joke, JokeSettings, StopableJoke } from "../joke";
|
||||
import $ from "jquery";
|
||||
import {Joke, JokeSettings} from '../joke';
|
||||
import $ from 'jquery';
|
||||
|
||||
/**
|
||||
* **Зеркало**
|
||||
@@ -8,19 +8,35 @@ import $ from "jquery";
|
||||
*
|
||||
* @author Kozhilya
|
||||
*/
|
||||
export class MirrorJoke extends Joke {
|
||||
id = 'mirror';
|
||||
export class MirrorJoke extends Joke<MirrorJokeSettings> {
|
||||
id = 'mirror';
|
||||
|
||||
settings = new MirrorJokeSettings;
|
||||
title = 'Зеркало';
|
||||
|
||||
start(): void {
|
||||
$('.post, .post h3, .post-links, .post-author, .post-content')
|
||||
.css('transform', 'scaleX(-1)').css('-moz-transform', 'scaleX(-1)').css('-moz-transform', 'scaleX(-1)').css('filter', 'FlipH').css('-ms-filter', '"FlipH"');
|
||||
$('.post-links, .post-links ul, .post h3 span').css('margin-left', '0');
|
||||
$('.post h3 span').css('margin-right', '25em');
|
||||
}
|
||||
description = 'Элементы форума отражаются (по крайней мере, пытаются)';
|
||||
|
||||
_settings = new MirrorJokeSettings();
|
||||
|
||||
/**
|
||||
* Запуск шутки
|
||||
*/
|
||||
start(): void {
|
||||
$('.post, .post h3, .post-links, .post-author, .post-content')
|
||||
.css('transform', 'scaleX(-1)')
|
||||
.css('-moz-transform', 'scaleX(-1)')
|
||||
.css('-moz-transform', 'scaleX(-1)')
|
||||
.css('filter', 'FlipH')
|
||||
.css('-ms-filter', '"FlipH"');
|
||||
$('.post-links, .post-links ul, .post h3 span').css('margin-left', '0');
|
||||
$('.post h3 span').css('margin-right', '25em');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Класс настроек для шутки "Зеркало"
|
||||
*/
|
||||
export class MirrorJokeSettings implements JokeSettings {
|
||||
chance: number = 30;
|
||||
enabled: boolean = false;
|
||||
|
||||
chance: number = 30;
|
||||
}
|
||||
+78
-57
@@ -1,5 +1,5 @@
|
||||
import { Joke, JokeSettings, StopableJoke } from "../joke";
|
||||
import $ from "jquery";
|
||||
import {JokeSettings, StoppableJoke} from '../joke';
|
||||
import $ from 'jquery';
|
||||
|
||||
/**
|
||||
* **Форумо-трясение**
|
||||
@@ -8,84 +8,105 @@ import $ from "jquery";
|
||||
*
|
||||
* @author Kozhilya
|
||||
*/
|
||||
export class ShakeJoke extends StopableJoke {
|
||||
id = 'shake';
|
||||
export class ShakeJoke extends StoppableJoke<ShakeJokeSettings> {
|
||||
id = 'shake';
|
||||
|
||||
settings = new ShakeJokeSettings;
|
||||
title = 'Форумо-трясение';
|
||||
|
||||
private interval_id: NodeJS.Timer;
|
||||
description = 'Страница форума начинает трястись';
|
||||
|
||||
private items: JQuery;
|
||||
_settings = new ShakeJokeSettings();
|
||||
|
||||
private data_name: string = 'april-fools-shake';
|
||||
private intervalId: NodeJS.Timeout;
|
||||
|
||||
rand(a: number): number {
|
||||
return a + this.settings.force * (Math.random() * 2 - 1);
|
||||
}
|
||||
private items: JQuery;
|
||||
|
||||
start(): void {
|
||||
this.items = $(this.settings.selector);
|
||||
private dataName: string = 'april-fools-shake';
|
||||
|
||||
this.items.each((_, elem) => {
|
||||
let data: ShakeJokeMargin = {};
|
||||
/**
|
||||
* Выбор случайной амплитуды
|
||||
* @param {number} a
|
||||
* @return {number}
|
||||
*/
|
||||
rand(a: number): number {
|
||||
return a + this.settings.force * (Math.random() * 2 - 1);
|
||||
}
|
||||
|
||||
this.settings.directions.forEach(d => {
|
||||
data[d] = parseInt($(elem).css('margin-' + d));
|
||||
});
|
||||
/**
|
||||
* Запуск шутки
|
||||
*/
|
||||
start(): void {
|
||||
this.items = $(this.settings.selector);
|
||||
|
||||
$(elem).data(this.data_name, data);
|
||||
});
|
||||
this.items.each((_, elem) => {
|
||||
const data: ShakeJokeMargin = {};
|
||||
|
||||
this.interval_id = setInterval(() => {
|
||||
this.items.each((_, elem) => {
|
||||
const data: ShakeJokeMargin = $(elem).data(this.data_name);
|
||||
this.settings.directions.forEach((d) => {
|
||||
data[d] = parseInt($(elem).css('margin-' + d));
|
||||
});
|
||||
|
||||
for (const entry of Object.entries(data)) {
|
||||
$(elem).css('margin-' + entry[0], this.rand(entry[1]))
|
||||
}
|
||||
});
|
||||
}, this.settings.interval);
|
||||
}
|
||||
$(elem).data(this.dataName, data);
|
||||
});
|
||||
|
||||
stop(): void {
|
||||
clearInterval(this.interval_id);
|
||||
this.intervalId = setInterval(() => {
|
||||
this.items.each((_, elem) => {
|
||||
const data: ShakeJokeMargin = $(elem).data(this.dataName);
|
||||
|
||||
this.items.each((_, elem) => {
|
||||
let data: ShakeJokeMargin = {};
|
||||
for (const entry of Object.entries(data)) {
|
||||
$(elem).css('margin-' + entry[0], this.rand(entry[1]));
|
||||
}
|
||||
});
|
||||
}, this.settings.interval);
|
||||
}
|
||||
|
||||
this.settings.directions.forEach(d => {
|
||||
$(elem).css('margin-' + d, data[d]);
|
||||
});
|
||||
/**
|
||||
* Остновка шутки
|
||||
*/
|
||||
stop(): void {
|
||||
clearInterval(this.intervalId);
|
||||
|
||||
$(elem).data(this.data_name, data);
|
||||
});
|
||||
}
|
||||
this.items.each((_, elem) => {
|
||||
const data: ShakeJokeMargin = {};
|
||||
|
||||
this.settings.directions.forEach((d) => {
|
||||
$(elem).css('margin-' + d, data[d]);
|
||||
});
|
||||
|
||||
$(elem).data(this.dataName, data);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Класс настроек для шутки "Форумо-трясение"
|
||||
*/
|
||||
export class ShakeJokeSettings implements JokeSettings {
|
||||
chance: number = 1;
|
||||
enabled: boolean = false;
|
||||
|
||||
/**
|
||||
* Направление тряски
|
||||
*/
|
||||
directions: string[] = ['top', 'left', 'bottom', 'left'];
|
||||
chance: number = 1;
|
||||
|
||||
/**
|
||||
* Селектор всех элементов, которые будут трястись
|
||||
*/
|
||||
selector: string = '[id^=pun]';
|
||||
/**
|
||||
* Направление тряски (тряска происходит путём изменения перечисленных
|
||||
* направлений margin на значение ±force от стандартного значения)
|
||||
*/
|
||||
directions: string[] = ['top', 'left', 'bottom', 'left'];
|
||||
|
||||
/**
|
||||
* Частота тряски
|
||||
*/
|
||||
interval: number = 100;
|
||||
/**
|
||||
* Сила тряски - насколько отклоняется элемент от стандартного значения
|
||||
*/
|
||||
force: number = 0.5;
|
||||
|
||||
/**
|
||||
* Сила тряски
|
||||
*/
|
||||
force: number = 0.5;
|
||||
/**
|
||||
* Селектор всех элементов, которые будут трястись
|
||||
*/
|
||||
selector: string = '[id^=pun]';
|
||||
|
||||
/**
|
||||
* Частота тряски
|
||||
*/
|
||||
interval: number = 100;
|
||||
}
|
||||
|
||||
export interface ShakeJokeMargin {
|
||||
[key: string]: number;
|
||||
[key: string]: number;
|
||||
}
|
||||
@@ -0,0 +1,419 @@
|
||||
import {Joke, JokeSettings} from '../joke';
|
||||
import $ from 'jquery';
|
||||
import {StupidTitleDefaultVariants} from '../data/stupid_title_variants';
|
||||
|
||||
/**
|
||||
* **Глупые статусы**
|
||||
*
|
||||
* Каждому пользователю даётся новый глупый статус
|
||||
*
|
||||
* @author Kozhilya
|
||||
*/
|
||||
export class StupidTitleJoke extends Joke<StupidTitleJokeSettings> {
|
||||
id = 'stupid_title';
|
||||
|
||||
title = 'Глупые статусы';
|
||||
|
||||
description = 'Каждому пользователю даётся новый глупый статус';
|
||||
|
||||
_settings = new StupidTitleJokeSettings();
|
||||
|
||||
private loadedGenders: { [key in string]: boolean } = null;
|
||||
private variantsGenerated = false;
|
||||
private seed = 0;
|
||||
|
||||
/**
|
||||
* Загрузка полов, если они не определены
|
||||
*/
|
||||
async loadGenders(): Promise<void> {
|
||||
const names = $('.pa-author a').
|
||||
map((_, elem) => elem.innerText).
|
||||
toArray().
|
||||
reduce((arr: string[], name: string) => {
|
||||
if (arr.indexOf(name) < 0) {
|
||||
arr.push(name);
|
||||
}
|
||||
return arr;
|
||||
}, []);
|
||||
|
||||
const url =
|
||||
'/api.php?' +
|
||||
$.param({method: 'users.get', fields: 'username,sex', username: names});
|
||||
const response = await fetch(url);
|
||||
const json = await response.json();
|
||||
|
||||
this.loadedGenders = {};
|
||||
|
||||
if (json.error) {
|
||||
return;
|
||||
}
|
||||
|
||||
json.response?.users.forEach(
|
||||
(element: { username: string, sex: string }) => {
|
||||
if (element.sex === '0') {
|
||||
return;
|
||||
}
|
||||
|
||||
this.loadedGenders[element.username] = element.sex === '1';
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Разрешение пола по имени
|
||||
* @param {string} name
|
||||
* @return {boolean}
|
||||
*/
|
||||
resolveGender(name: string): boolean {
|
||||
if (this.settings.gender_resolver !== null) {
|
||||
return !!this.settings.gender_resolver.call(this, name);
|
||||
}
|
||||
|
||||
if (name in this.loadedGenders) {
|
||||
return !!this.loadedGenders[name];
|
||||
}
|
||||
|
||||
this.resetSeed();
|
||||
return this.selectRandom(name, {m: 1, f: 1}) === 'm';
|
||||
}
|
||||
|
||||
/**
|
||||
* Запуск шутки
|
||||
*/
|
||||
async start(): Promise<void> {
|
||||
if (!this.variantsGenerated) {
|
||||
StupidTitleDefaultVariants(this.settings.variants);
|
||||
this.variantsGenerated = true;
|
||||
}
|
||||
|
||||
await this.loadGenders();
|
||||
|
||||
const cache: {[key in string]: string} = {};
|
||||
|
||||
$('.post-author').each((_, root) => {
|
||||
const name = $('.pa-author a', root).text();
|
||||
if (!name) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!(name in cache)) {
|
||||
this.resetSeed();
|
||||
cache[name] = this.generatePhrase(name);
|
||||
}
|
||||
|
||||
$('.pa-title', root).text(cache[name]);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Сгенерировать фразу
|
||||
* @param {string} name Имя, кому необходимо сгенерировать фразу
|
||||
* @return {string} Итоговая фраза
|
||||
*/
|
||||
generatePhrase(name: string): string {
|
||||
const gender = this.resolveGender(name);
|
||||
const phrase = this.selectRandom(name, this.settings.schemas);
|
||||
|
||||
return this.capitalizeFirst(
|
||||
phrase.replace(/\{(.+?)}/g, (_, type: string) => {
|
||||
const variants = this.buildVariants(type, gender);
|
||||
|
||||
return this.selectRandom(name, variants);
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Функция хеширования
|
||||
*
|
||||
* https://github.com/bryc/code/blob/master/jshash/experimental/cyrb53.js
|
||||
*
|
||||
* cyrb53 (c) 2018 bryc (github.com/bryc)
|
||||
* A fast and simple hash function with decent collision resistance.
|
||||
* Largely inspired by MurmurHash2/3, but with a focus on speed/simplicity.
|
||||
* Public domain. Attribution appreciated.
|
||||
*
|
||||
* @param {string} name Имя
|
||||
* @param {number} seed Зерно случайности
|
||||
* @param {limit} limit Максимальное значение
|
||||
* @return {number} Хеш-значение
|
||||
* @private
|
||||
*/
|
||||
private prng(name: string, seed: number, limit: number): number {
|
||||
let h1 = 0xdeadbeef ^ seed;
|
||||
let h2 = 0x41c6ce57 ^ seed;
|
||||
for (let i = 0, ch; i < name.length; i++) {
|
||||
ch = name.charCodeAt(i);
|
||||
h1 = Math.imul(h1 ^ ch, 2654435761);
|
||||
h2 = Math.imul(h2 ^ ch, 1597334677);
|
||||
}
|
||||
h1 =
|
||||
Math.imul(h1 ^ (h1 >>> 16), 2246822507) ^
|
||||
Math.imul(h2 ^ (h2 >>> 13), 3266489909);
|
||||
h2 =
|
||||
Math.imul(h2 ^ (h2 >>> 16), 2246822507) ^
|
||||
Math.imul(h1 ^ (h1 >>> 13), 3266489909);
|
||||
|
||||
const value = 4294967296 * (2097151 & h2) + (h1 >>> 0);
|
||||
|
||||
return Math.floor(Math.pow(Math.sin(value), 2) * limit) % limit;
|
||||
}
|
||||
|
||||
/**
|
||||
* Сброс зерна случайности
|
||||
* @private
|
||||
*/
|
||||
private resetSeed(): void {
|
||||
const date = new Date();
|
||||
const time = date.getUTCHours() * 60 + date.getUTCMinutes();
|
||||
|
||||
if (this.settings.change_frequency === 0) {
|
||||
this.seed = time * 60 + date.getUTCSeconds();
|
||||
return;
|
||||
}
|
||||
|
||||
this.seed = Math.floor(time / this.settings.change_frequency);
|
||||
}
|
||||
|
||||
/**
|
||||
* Выбор случайного слова
|
||||
* @param {string} name
|
||||
* @param {StupidTitleWeightedVariants} variants
|
||||
* @return {string}
|
||||
* @private
|
||||
*/
|
||||
private selectRandom(
|
||||
name: string,
|
||||
variants: StupidTitleWeightedVariants,
|
||||
): string {
|
||||
const elements = Object.keys(variants);
|
||||
const totalWeight = elements.reduce(
|
||||
(sum, element) => sum + variants[element],
|
||||
0,
|
||||
);
|
||||
const randomWeight = this.prng(name, this.seed++, totalWeight);
|
||||
|
||||
let currentWeight = 0;
|
||||
for (const element of elements) {
|
||||
currentWeight += variants[element];
|
||||
if (randomWeight <= currentWeight) {
|
||||
return element;
|
||||
}
|
||||
}
|
||||
|
||||
throw new Error('Тебя быть тут не должно...');
|
||||
}
|
||||
|
||||
/**
|
||||
* Выбор случайной фразы
|
||||
* @param {string} type
|
||||
* @param {boolean} gender
|
||||
* @return {StupidTitleWeightedVariants}
|
||||
* @private
|
||||
*/
|
||||
private buildVariants(
|
||||
type: string,
|
||||
gender: boolean,
|
||||
): StupidTitleWeightedVariants {
|
||||
const result: StupidTitleWeightedVariants = {};
|
||||
const variants: StupidTitleVariant = this.settings.variants[type];
|
||||
|
||||
for (const iterator of Object.entries(variants)) {
|
||||
const word = iterator[0];
|
||||
const wordSettings = iterator[1];
|
||||
|
||||
if (!wordSettings.enabled || wordSettings.gender === !gender) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!this.settings.rude && wordSettings.rude) {
|
||||
continue;
|
||||
}
|
||||
|
||||
result[word] = wordSettings.weight;
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Сделать первую буква слова заглавной
|
||||
* @param {string} str Слово
|
||||
* @return {string} Исправленное слово
|
||||
* @private
|
||||
*/
|
||||
private capitalizeFirst(str: string): string {
|
||||
return str.charAt(0).toUpperCase() + str.slice(1);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Определение пола по имени.
|
||||
* @param {string} name Имя персонажа
|
||||
* @return {boolean | null} true, если мужской род; false, если женский род
|
||||
*/
|
||||
type GenderResolverFunction = (name: string) => (boolean | null);
|
||||
|
||||
/**
|
||||
* Класс настроек для шутки "Глупые статусы"
|
||||
*/
|
||||
export class StupidTitleJokeSettings implements JokeSettings {
|
||||
enabled: boolean = true;
|
||||
chance: number = 100;
|
||||
|
||||
/**
|
||||
* Определение пола по имени.
|
||||
* По умолчанию род устанавливается случайным образом
|
||||
*
|
||||
* Если установлено значение `null`, система попытается загрузить пол,
|
||||
* установленный в настройках аккаунта. Если этот пол определить невозможно,
|
||||
* то он будет выбран случайно. Эта настройка позволяет изменить это
|
||||
* поведение, если ваш форум обладает ролевой системой, в которой пол
|
||||
* определяется иным образом.
|
||||
*/
|
||||
gender_resolver: GenderResolverFunction | null = null;
|
||||
|
||||
/**
|
||||
* Разрешить возможно оскорбительные варианты
|
||||
*/
|
||||
rude: boolean = false;
|
||||
|
||||
/**
|
||||
* Частота смены статусов (в минутах)
|
||||
* Установите значение 0, что бы обновление происходило при
|
||||
* каждом обновлении страницы
|
||||
*/
|
||||
change_frequency: number = 5;
|
||||
|
||||
/**
|
||||
* Разрешённые схемы формирования статусов
|
||||
*
|
||||
* {adj} - прилагательное
|
||||
* {noun} - имя существительно
|
||||
* {verb} - глагол
|
||||
* {adv} - наречие
|
||||
* {place} - дополнение места
|
||||
* {p} - знак препинания
|
||||
*/
|
||||
schemas: StupidTitleWeightedVariants = {
|
||||
'{adj} {noun}{p}': 10,
|
||||
'{noun} {adj}{p}': 3,
|
||||
'{adj}, {adj} {noun}{p}': 10,
|
||||
'{adj} и очень {adj} {noun}{p}': 10,
|
||||
'{adj} {noun} {verb}{p}': 10,
|
||||
'{adj} {noun} {adv} {verb}{p}': 5,
|
||||
'{adj} {noun} {verb} {place}{p}': 5,
|
||||
'{adj} {noun} {adv} {verb} {place}{p}': 5,
|
||||
};
|
||||
|
||||
/**
|
||||
* Вариант слов
|
||||
* Смотрите раздел описания скрипта, что бы настроить это поле
|
||||
*/
|
||||
variants: StupidTitleVariants = {
|
||||
adj: {},
|
||||
noun: {},
|
||||
verb: {},
|
||||
place: {},
|
||||
adv: {},
|
||||
p: {
|
||||
'': new StupidTitleSetting({weight: 15}),
|
||||
'.': new StupidTitleSetting({weight: 10}),
|
||||
'...': new StupidTitleSetting({weight: 5}),
|
||||
'!': new StupidTitleSetting({weight: 5}),
|
||||
'!!!': new StupidTitleSetting({weight: 2}),
|
||||
'?': new StupidTitleSetting({weight: 2}),
|
||||
'?!': new StupidTitleSetting({weight: 1}),
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export interface StupidTitleVariants {
|
||||
[key: string]: StupidTitleVariant;
|
||||
}
|
||||
|
||||
interface StupidTitleVariant {
|
||||
[key: string]: StupidTitleSetting;
|
||||
}
|
||||
|
||||
interface StupidTitleWeightedVariants {
|
||||
[key: string]: number;
|
||||
}
|
||||
|
||||
type StupidTitleSettingInitializer = {
|
||||
weight?: number;
|
||||
enabled?: boolean;
|
||||
rude?: boolean;
|
||||
gender?: boolean | null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Класс настройки
|
||||
*/
|
||||
class StupidTitleSetting {
|
||||
weight: number = 10;
|
||||
|
||||
enabled: boolean = true;
|
||||
|
||||
rude: boolean = false;
|
||||
|
||||
gender: boolean | null = null;
|
||||
|
||||
/**
|
||||
* Создание класса
|
||||
* @param {StupidTitleSettingInitializer} options
|
||||
*/
|
||||
constructor(options: StupidTitleSettingInitializer) {
|
||||
this.weight ??= options.weight;
|
||||
this.enabled ??= options.enabled;
|
||||
this.rude ??= options.rude;
|
||||
this.gender ??= options.gender;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Метод создания списка слов
|
||||
* @param {boolean | null} gender
|
||||
* @param {string} input
|
||||
* @return {StupidTitleVariant}
|
||||
* @constructor
|
||||
*/
|
||||
export function BuildStupidTitleVariant(
|
||||
gender: boolean | null,
|
||||
input: string,
|
||||
): StupidTitleVariant {
|
||||
const result: StupidTitleVariant = {};
|
||||
const words = input.split(',').map((word) => word.trim());
|
||||
|
||||
words.forEach((word) => {
|
||||
const cleanWord = word.replace(/[^А-ЯЁа-яёA-Za-z\- ]/g, '');
|
||||
|
||||
if (cleanWord === '') {
|
||||
return;
|
||||
}
|
||||
|
||||
result[cleanWord] = new StupidTitleSetting({
|
||||
gender,
|
||||
rude: word.indexOf('$') >= 0,
|
||||
enabled: word.indexOf('!') < 0,
|
||||
weight:
|
||||
10 *
|
||||
(word.indexOf('$') >= 0 ? 0.5 : 1) *
|
||||
(word.indexOf('%') >= 0 ? 0.5 : 1) *
|
||||
(word.indexOf('^') >= 0 ? 2 : 1),
|
||||
});
|
||||
});
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface Window {
|
||||
BuildStupidTitleVariant(
|
||||
gender: boolean | null,
|
||||
input: string,
|
||||
): StupidTitleVariant;
|
||||
}
|
||||
}
|
||||
|
||||
window.BuildStupidTitleVariant = BuildStupidTitleVariant;
|
||||
|
||||
@@ -0,0 +1,150 @@
|
||||
import {d2} from '../common';
|
||||
|
||||
/**
|
||||
* Класс вектора
|
||||
*/
|
||||
export class Vector {
|
||||
/**
|
||||
* Координата X
|
||||
*/
|
||||
x: number;
|
||||
|
||||
/**
|
||||
* Координата Y
|
||||
*/
|
||||
y: number;
|
||||
|
||||
/**
|
||||
* Конструктор вектора
|
||||
* @param {number} x Координата X
|
||||
* @param {number} y Координата Y
|
||||
*/
|
||||
constructor(x: number = 0, y: number = 0) {
|
||||
this.x = x;
|
||||
this.y = y;
|
||||
}
|
||||
|
||||
/**
|
||||
* Угол наклона вектора
|
||||
*/
|
||||
get angle() {
|
||||
return Math.atan2(this.y, this.x);
|
||||
}
|
||||
|
||||
/**
|
||||
* Угол наклона вектора
|
||||
* @param {number} angle
|
||||
*/
|
||||
set angle(angle: number) {
|
||||
const length = this.length;
|
||||
this.x = Math.cos(angle) * length;
|
||||
this.y = Math.sin(angle) * length;
|
||||
}
|
||||
|
||||
/**
|
||||
* Длина вектора
|
||||
*/
|
||||
get length() {
|
||||
return Math.distance(this.x, this.y);
|
||||
}
|
||||
|
||||
/**
|
||||
* Длина вектора
|
||||
* @param {number} length
|
||||
*/
|
||||
set length(length: number) {
|
||||
const angle = this.angle;
|
||||
this.x = Math.cos(angle) * length;
|
||||
this.y = Math.sin(angle) * length;
|
||||
}
|
||||
|
||||
/**
|
||||
* Сумма векторов
|
||||
* @param {Vector} a Первый вектор
|
||||
* @param {Vector} b Второй вектор
|
||||
* @return {Vector} Вектор суммы
|
||||
*/
|
||||
static addVectors(a: Vector, b: Vector): Vector {
|
||||
return new Vector(a.x + b.x, a.y + b.y);
|
||||
}
|
||||
|
||||
/**
|
||||
* Скалярное умножение векторов
|
||||
* @param {Vector} a Первый вектор
|
||||
* @param {Vector} b Второй вектор
|
||||
* @return {number} Скалярное произведение векторов
|
||||
*/
|
||||
static dotProduct(a: Vector, b: Vector): number {
|
||||
return a.x * b.x + a.y * b.y;
|
||||
}
|
||||
|
||||
/**
|
||||
* Создать вектор по углу наклона и длине
|
||||
* @param {number} angle Угол наклона
|
||||
* @param {number} length Длина
|
||||
* @return {Vector} Вектор суммы
|
||||
*/
|
||||
static fromLengthAngle(angle: number, length: number): Vector {
|
||||
const v = new Vector();
|
||||
v.length = length;
|
||||
v.angle = angle;
|
||||
return v;
|
||||
}
|
||||
|
||||
/**
|
||||
* Нахождение противоположного вектора
|
||||
* @return {Vector} Противоположный вектор
|
||||
*/
|
||||
reverse(): Vector {
|
||||
return new Vector(-this.x, -this.y);
|
||||
}
|
||||
|
||||
/**
|
||||
* Прибавить вектор
|
||||
* @param {Vector} a Второе слагаемое
|
||||
* @return {Vector} Вектор суммы
|
||||
*/
|
||||
add(a: Vector): Vector {
|
||||
return Vector.addVectors(this, a);
|
||||
}
|
||||
|
||||
/**
|
||||
* Умножение вектора на число
|
||||
* @param {number} a Скаляр
|
||||
* @return {Vector} Вектор-результат
|
||||
*/
|
||||
multiply(a: number): Vector {
|
||||
return new Vector(a * this.x, a * this.y);
|
||||
}
|
||||
|
||||
/**
|
||||
* Преобразование в CSS-преобразование
|
||||
* @return {string} CSS-преобразование
|
||||
*/
|
||||
toTransform(): string {
|
||||
return `translate(${this.x}px, ${this.y}px)`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Строковое представление
|
||||
* @return {string}
|
||||
*/
|
||||
toString(): string {
|
||||
return d2`Vector(x=${this.x}, y=${this.y}, ` +
|
||||
d2`length=${this.length}, angle=${this.angle})`;
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface Math {
|
||||
/**
|
||||
* Расчёт длины вектора
|
||||
* @param {number} x Координата X вектора
|
||||
* @param {number} y Координата X вектора
|
||||
* @return {number} Длина вектора
|
||||
*/
|
||||
distance(x: number, y: number): number;
|
||||
}
|
||||
}
|
||||
|
||||
Math.distance = (x, y) => Math.sqrt(x * x + y * y);
|
||||
+36
-22
@@ -1,29 +1,43 @@
|
||||
import { AprilFoolsJokeClass } from "./handler";
|
||||
import { Joke } from "./joke";
|
||||
import { ShakeJoke } from "./jokes/shake";
|
||||
import { HumanitarianHellJoke } from "./jokes/humanitarian_hell";
|
||||
import { MirrorJoke } from "./jokes/mirror";
|
||||
import { CarnivalJoke } from "./jokes/carnival";
|
||||
import { CrazyLettersJoke } from "./jokes/crazy_letters";
|
||||
import { CrazyPunctuationJoke } from "./jokes/crazy_punctuation";
|
||||
import {JokerClass} from './joker';
|
||||
import {Joke, JokeSettings} from './joke';
|
||||
import {ShakeJoke} from './jokes/shake';
|
||||
import {HumanitarianHellJoke} from './jokes/humanitarian_hell';
|
||||
import {MirrorJoke} from './jokes/mirror';
|
||||
import {CarnivalJoke} from './jokes/carnival';
|
||||
import {CrazyLettersJoke} from './jokes/crazy_letters';
|
||||
import {LetterChaosJoke} from './jokes/letter_chaos';
|
||||
import {CursorEffectsJoke} from './jokes/cursor';
|
||||
import {StupidTitleJoke} from './jokes/stupid_title';
|
||||
import {InvertedScrollJoke} from './jokes/inverted_scroll';
|
||||
import {LatinizationJoke} from './jokes/latinization';
|
||||
import {FreshGrassJoke} from './jokes/fresh_grass';
|
||||
|
||||
(window as any).AprilJokes = ((jokes: JokeClassList): AprilFoolsJokeClass => {
|
||||
const cl = new AprilFoolsJokeClass();
|
||||
declare global {
|
||||
interface Window { AprilJokes: JokerClass; }
|
||||
}
|
||||
|
||||
jokes.forEach(joke => {
|
||||
cl.add(new joke());
|
||||
});
|
||||
window.AprilJokes = ((jokes: JokeList): JokerClass => {
|
||||
const cl = new JokerClass();
|
||||
|
||||
cl.regularStart();
|
||||
jokes.forEach((CustomJoke) => {
|
||||
cl.add(new CustomJoke());
|
||||
});
|
||||
|
||||
return cl;
|
||||
$(() => cl.regularStart());
|
||||
|
||||
return cl;
|
||||
})([
|
||||
ShakeJoke,
|
||||
HumanitarianHellJoke,
|
||||
MirrorJoke,
|
||||
CarnivalJoke,
|
||||
CrazyLettersJoke,
|
||||
CrazyPunctuationJoke,
|
||||
ShakeJoke,
|
||||
HumanitarianHellJoke,
|
||||
MirrorJoke,
|
||||
CarnivalJoke,
|
||||
CrazyLettersJoke,
|
||||
LetterChaosJoke,
|
||||
CursorEffectsJoke,
|
||||
StupidTitleJoke,
|
||||
InvertedScrollJoke,
|
||||
LatinizationJoke,
|
||||
FreshGrassJoke,
|
||||
]);
|
||||
|
||||
interface JokeClassList extends Array<new () => Joke> {}
|
||||
interface JokeList extends Array<new () => Joke<JokeSettings>> {}
|
||||
|
||||
Vendored
+108
@@ -0,0 +1,108 @@
|
||||
/**
|
||||
* Настройки формы ответа MyBB
|
||||
*/
|
||||
export type MybbEditor = {
|
||||
settings: MybbEditorSettings;
|
||||
[key: string]: MybbEditorTag | MybbEditorSettings;
|
||||
}
|
||||
|
||||
/**
|
||||
* Общий объект настройки тега MyBB
|
||||
*/
|
||||
export class MybbEditorTag {
|
||||
/**
|
||||
* Название кнопки.
|
||||
*/
|
||||
name: string;
|
||||
|
||||
/**
|
||||
* Функция, которая будет вызвана при нажатии кнопки.
|
||||
*/
|
||||
onclick: () => void;
|
||||
}
|
||||
|
||||
/**
|
||||
* Общий объект настройки формы ответа MyBB
|
||||
*/
|
||||
export type MybbEditorSettings = {
|
||||
autofocus: string
|
||||
}
|
||||
|
||||
/**
|
||||
* Информация о теме
|
||||
*/
|
||||
export class MybbTopic {
|
||||
/**
|
||||
* Тема сообщения
|
||||
*/
|
||||
subject: string;
|
||||
|
||||
/**
|
||||
* Флаг закрытия
|
||||
* @type "0"|"1"
|
||||
*/
|
||||
closed: string;
|
||||
|
||||
/**
|
||||
* Количество ответов в теме
|
||||
*/
|
||||
num_replies: string;
|
||||
|
||||
/**
|
||||
* Количество просмотров темы
|
||||
*/
|
||||
num_views: string;
|
||||
|
||||
/**
|
||||
* ID форума
|
||||
*/
|
||||
forum_id: string;
|
||||
|
||||
/**
|
||||
* Имя форума
|
||||
*/
|
||||
forum_name: string;
|
||||
|
||||
/**
|
||||
* Список модераторов форума,
|
||||
* ключ - ID пользователя, значение - имя модератора
|
||||
*/
|
||||
moderators: {
|
||||
[key in string]: string
|
||||
};
|
||||
|
||||
/**
|
||||
* Объект языков
|
||||
*/
|
||||
language: {
|
||||
share_legend: string
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Настройки видео-хостингов
|
||||
*/
|
||||
// eslint-disable-next-line
|
||||
export class MYBB_vsc {
|
||||
// eslint-disable-next-line
|
||||
cH: any;
|
||||
RG: { [key: string]: MYBB_vsc_item };
|
||||
checkHost: (hostingItem: MYBB_vsc_item, input: string) => boolean;
|
||||
clickHost: (element: HTMLAnchorElement) => void;
|
||||
parse: () => void;
|
||||
isLink: (input: string) => boolean;
|
||||
message: (html: string) => void;
|
||||
reInit: () => void;
|
||||
}
|
||||
|
||||
/**
|
||||
* Настройка отдельного видео-хостинга
|
||||
*/
|
||||
// eslint-disable-next-line
|
||||
type MYBB_vsc_item = {
|
||||
l: '0' | '1';
|
||||
t: string;
|
||||
x: RegExp;
|
||||
nf: string;
|
||||
}
|
||||
Vendored
+28
@@ -0,0 +1,28 @@
|
||||
/**
|
||||
* Объект утилит ForumDUtils
|
||||
*/
|
||||
export class ForumDUtils {
|
||||
/**
|
||||
* Проверяет, разрешён ли запуск скрипта
|
||||
*/
|
||||
check(): Promise<boolean>;
|
||||
|
||||
/**
|
||||
* Ошибка, сообщающая о невозможности запуска скрипта
|
||||
*/
|
||||
error(): Error;
|
||||
|
||||
/**
|
||||
* Набор настроек скриптов
|
||||
*/
|
||||
scripts: {
|
||||
[key in string]: ForumDUtilsScript;
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Класс настроек скриптов
|
||||
*/
|
||||
export abstract class ForumDUtilsScript {
|
||||
|
||||
}
|
||||
Vendored
+4
@@ -0,0 +1,4 @@
|
||||
declare module '*.html' {
|
||||
const content: string;
|
||||
export default content;
|
||||
}
|
||||
Vendored
+335
@@ -0,0 +1,335 @@
|
||||
// eslint-disable-next-line camelcase
|
||||
import {MYBB_vsc, MybbEditor, MybbTopic} from './FORUM';
|
||||
import {ForumDUtils} from './ForumDUtils';
|
||||
|
||||
declare global {
|
||||
interface Window {
|
||||
/**
|
||||
* Объект для работы с ForumD Utils.
|
||||
*/
|
||||
FDU?: ForumDUtils;
|
||||
|
||||
/**
|
||||
* Объект настроек форума.
|
||||
*/
|
||||
FORUM: {
|
||||
topic?: MybbTopic;
|
||||
editor?: MybbEditor;
|
||||
};
|
||||
|
||||
/**
|
||||
* Вставить в форму цитату в теге [quote].
|
||||
* @param {string} author Автор сообщения
|
||||
* @param {number} postId ID сообщения, из которого происходит цитата
|
||||
*/
|
||||
quote(author: string, postId: number): void;
|
||||
|
||||
/**
|
||||
* Вставка BB-кода.
|
||||
* @param {string} tagStart Текст, что будет вставлен до выделения
|
||||
* @param {string} tagEnd Текст, что будет вставлен после выделения
|
||||
*/
|
||||
bbcode(tagStart: string, tagEnd: string): void;
|
||||
|
||||
/**
|
||||
* Изменить видимость служебной области.
|
||||
* @param {string} areaName Идентификатор области
|
||||
*/
|
||||
changeVisibility(areaName: string): void;
|
||||
|
||||
/**
|
||||
* Вставка сообщения.
|
||||
* @param {string} value
|
||||
*/
|
||||
insert(value: string): void;
|
||||
|
||||
/**
|
||||
* Вставить смайл.
|
||||
* @param {string} emote Код смайла
|
||||
*/
|
||||
smile(emote: string): void;
|
||||
|
||||
/**
|
||||
* Вставка тега таблицы.
|
||||
* @param {number} rows Количество строк
|
||||
* @param {number} columns Количество столбцов
|
||||
*/
|
||||
// eslint-disable-next-line camelcase
|
||||
tag_table(rows: number, columns: number): void;
|
||||
|
||||
/**
|
||||
* Метод выбора видео-хостинга.
|
||||
* @param {HTMLElement} element Элемент
|
||||
* @param {Event|Event[]} event Событие
|
||||
*/
|
||||
// eslint-disable-next-line camelcase
|
||||
MYBB_vsi(element: HTMLElement, event: Event|Event[]): void;
|
||||
|
||||
/**
|
||||
* Объект с правилами для видео-тегов.
|
||||
*/
|
||||
// eslint-disable-next-line camelcase
|
||||
MYBB_vsc: MYBB_vsc;
|
||||
|
||||
/**
|
||||
* Вставка загруженного изображения.
|
||||
* @param {HTMLElement | JQuery} element Элемент области вставки
|
||||
*/
|
||||
insertUploadedImage(element: HTMLElement | JQuery): void;
|
||||
|
||||
/**
|
||||
* Вставка тега спойлера.
|
||||
* @param {string} spoilerType Тип спойлера
|
||||
*/
|
||||
// eslint-disable-next-line camelcase
|
||||
tag_spoiler(spoilerType: string): void;
|
||||
|
||||
/**
|
||||
* Вставка символа клавиатуры.
|
||||
* @param {string} character Вставляемый символ
|
||||
*/
|
||||
keyboard(character: string): void;
|
||||
|
||||
/**
|
||||
* Вставить обращение к пользователю.
|
||||
* @param {string} target Имя пользователя
|
||||
*/
|
||||
to(target: string): void;
|
||||
|
||||
// ===== Переменные форума =====
|
||||
|
||||
/**
|
||||
* ID форума.
|
||||
*/
|
||||
BoardID: number;
|
||||
|
||||
/**
|
||||
* Категория форума в каталоге.
|
||||
*/
|
||||
BoardCat: number;
|
||||
|
||||
/**
|
||||
* Название стиля форума или `Custom`, если используется свой стиль.
|
||||
*/
|
||||
BoardStyle: string;
|
||||
|
||||
/**
|
||||
* API-код пользователя.
|
||||
*/
|
||||
ForumAPITicket: string;
|
||||
|
||||
/**
|
||||
* ID группы пользователя.
|
||||
* `1` - это группа администраторов, `2` - модераторов, `3` - гостей.
|
||||
*/
|
||||
GroupID: number;
|
||||
|
||||
/**
|
||||
* Название группы пользователя.
|
||||
*/
|
||||
GroupTitle: string;
|
||||
|
||||
/**
|
||||
* Статус группы пользователя.
|
||||
*/
|
||||
GroupUserTitle: string;
|
||||
|
||||
/**
|
||||
* ID партнёра форума.
|
||||
*/
|
||||
PartnerID: number;
|
||||
|
||||
/**
|
||||
* Время запроса к форуму.
|
||||
*/
|
||||
RequestTime: number;
|
||||
|
||||
/**
|
||||
* Префикс URL для доступа к статическим файлам.
|
||||
* Не имеет заключающей косой черты.
|
||||
*/
|
||||
StaticURL: string;
|
||||
|
||||
/**
|
||||
* Префикс URL для доступа к аватарам.
|
||||
* Не имеет заключающей косой черты.
|
||||
*/
|
||||
AvatarsURL: string;
|
||||
|
||||
/**
|
||||
* Префикс URL для доступа к файлам форума.
|
||||
* Не имеет заключающей косой черты.
|
||||
*/
|
||||
FilesURL: string;
|
||||
|
||||
/**
|
||||
* Префикс URL для доступа к скриптам.
|
||||
* Не имеет заключающей косой черты.
|
||||
*/
|
||||
ScriptsURL: string;
|
||||
|
||||
/**
|
||||
* Префикс URL для доступа к загружаемым файлам.
|
||||
* Не имеет заключающей косой черты.
|
||||
*/
|
||||
UploadsURL: string;
|
||||
|
||||
/**
|
||||
* ID пользователя.
|
||||
*/
|
||||
UserID?: number;
|
||||
|
||||
/**
|
||||
* Логин пользователя.
|
||||
*/
|
||||
UserLogin?: string;
|
||||
|
||||
/**
|
||||
* Имя пользователя.
|
||||
*/
|
||||
UserName?: string;
|
||||
|
||||
/**
|
||||
* Статус пользователя
|
||||
*/
|
||||
UserTitle?: string;
|
||||
|
||||
/**
|
||||
* Пол пользователя.
|
||||
* 0 - мужской, 1 - женский.
|
||||
*/
|
||||
UserSex?: number;
|
||||
|
||||
/**
|
||||
* Возраст пользователя.
|
||||
*/
|
||||
UserAge?: number;
|
||||
|
||||
/**
|
||||
* Дата рождения пользователя.
|
||||
*/
|
||||
UserBirthDate?: string;
|
||||
|
||||
/**
|
||||
* Ссылка на аватар пользователя.
|
||||
* Не содержит домен, но имеет начальную косую черту.
|
||||
* @see AvatarsURL
|
||||
*/
|
||||
UserAvatar?: string;
|
||||
|
||||
/**
|
||||
* Время регистрации пользователя.
|
||||
*/
|
||||
UserRegistered?: number;
|
||||
|
||||
/**
|
||||
* Код приглашения пользователя.
|
||||
*/
|
||||
UserInviteCode?: string;
|
||||
|
||||
/**
|
||||
* Количество сообщений пользователя.
|
||||
*/
|
||||
UserPosts?: number;
|
||||
|
||||
/**
|
||||
* Время последнего сообщения.
|
||||
*/
|
||||
UserLastPost?: number;
|
||||
|
||||
/**
|
||||
* Время последнего посещения.
|
||||
*/
|
||||
UserLastVisit?: number;
|
||||
|
||||
/**
|
||||
* Время пребывания пользователя на форуме.
|
||||
*/
|
||||
UserTotalTime?: number;
|
||||
|
||||
/**
|
||||
* Количество плюсов, полученных пользователем.
|
||||
*/
|
||||
UserRespectPlus?: number;
|
||||
|
||||
/**
|
||||
* Количество минусов, полученных пользователем.
|
||||
*/
|
||||
UserRespectMinus?: number;
|
||||
|
||||
/**
|
||||
* Количество плюсов, поставленных пользователем.
|
||||
*/
|
||||
UserPositivePlus?: number;
|
||||
|
||||
/**
|
||||
* Количество плюсов, поставленных пользователем.
|
||||
*/
|
||||
UserPositiveMinus?: number;
|
||||
|
||||
/**
|
||||
* Уникальный ID пользователя.
|
||||
*/
|
||||
UserUniqueID?: string;
|
||||
|
||||
/**
|
||||
* Язык пользователя.
|
||||
*/
|
||||
UserLanguage?: string;
|
||||
|
||||
/**
|
||||
* Путь к папке.
|
||||
*/
|
||||
UserUploadsDir?: string;
|
||||
|
||||
/**
|
||||
* Дополнительное поле форума 1.
|
||||
*/
|
||||
UserFld1?: string;
|
||||
|
||||
/**
|
||||
* Дополнительное поле форума 2.
|
||||
*/
|
||||
UserFld2?: string;
|
||||
|
||||
/**
|
||||
* Дополнительное поле форума 3.
|
||||
*/
|
||||
UserFld3?: string;
|
||||
|
||||
/**
|
||||
* Дополнительное поле форума 4.
|
||||
*/
|
||||
UserFld4?: string;
|
||||
|
||||
/**
|
||||
* Дополнительное поле форума 5.
|
||||
*/
|
||||
UserFld5?: string;
|
||||
|
||||
/**
|
||||
* Дополнительное поле форума 6.
|
||||
*/
|
||||
UserFld6?: string;
|
||||
|
||||
/**
|
||||
* Дополнительное поле форума 7.
|
||||
*/
|
||||
UserFld7?: string;
|
||||
|
||||
/**
|
||||
* Дополнительное поле форума 8.
|
||||
*/
|
||||
UserFld8?: string;
|
||||
|
||||
/**
|
||||
* Дополнительное поле форума 9.
|
||||
*/
|
||||
UserFld9?: string;
|
||||
}
|
||||
|
||||
interface JQuery {
|
||||
mybbModal(data: object): JQuery;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,88 @@
|
||||
import './types/html.d';
|
||||
import './types/mybb.d';
|
||||
|
||||
import {JokerClass} from './joker';
|
||||
import $ from 'jquery';
|
||||
import buttonHTML from './html/button.html';
|
||||
import stylesHTML from './html/styles.html';
|
||||
import windowHTML from './html/window.html';
|
||||
|
||||
/**
|
||||
* Класс интерфейса настроек
|
||||
*/
|
||||
export class JokerUI {
|
||||
joker: JokerClass;
|
||||
|
||||
/**
|
||||
* Создание класса интерфейса настроек
|
||||
*
|
||||
* @param {JokerClass} joker
|
||||
*/
|
||||
constructor(joker: JokerClass) {
|
||||
this.joker = joker;
|
||||
|
||||
$('link[rel="stylesheet"]').eq(0).before(stylesHTML);
|
||||
|
||||
const button = this.button;
|
||||
|
||||
button.hide();
|
||||
|
||||
button.mybbModal({
|
||||
title: 'Настройки первоапрельских шуток',
|
||||
content: () => this.render(),
|
||||
closer: true,
|
||||
noscroll: true,
|
||||
escClose: true,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Основная кнопка, открывающая настройки
|
||||
* @return {JQuery<HTMLButtonElement>}
|
||||
*/
|
||||
get button(): JQuery<HTMLButtonElement> {
|
||||
let button = $('.april-fools__button');
|
||||
|
||||
if (button.length === 0) {
|
||||
button = $(buttonHTML).appendTo('body');
|
||||
}
|
||||
|
||||
return button as JQuery<HTMLButtonElement>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Показать кнопку настроек
|
||||
*/
|
||||
show() {
|
||||
this.button.show();
|
||||
}
|
||||
|
||||
/**
|
||||
* Создать окно настроек
|
||||
* @return {JQuery<HTMLDivElement>}
|
||||
*/
|
||||
render(): JQuery<HTMLDivElement> {
|
||||
const result = $('<div></div>').html(windowHTML) as JQuery<HTMLDivElement>;
|
||||
const rowTemplate = $('#april-fools__window-row', result).html();
|
||||
const jokeContainer = $('.april-fools__jokes', result);
|
||||
|
||||
for (const joke of Object.values(this.joker.jokes)) {
|
||||
const row = $(eval('`' + rowTemplate + '`'));
|
||||
$('input[type="checkbox"]', row)
|
||||
.prop('checked', joke.settings.enabled)
|
||||
.on('click', (e) => {
|
||||
const target = (e.currentTarget as HTMLInputElement);
|
||||
this.joker.toggleJoke(joke.id, target.checked);
|
||||
}
|
||||
);
|
||||
jokeContainer.append(row);
|
||||
}
|
||||
|
||||
$('button', result).on('click', async () => {
|
||||
await this.joker.clearUserStates();
|
||||
window.location.reload();
|
||||
});
|
||||
|
||||
return $('.april-fools__content', result);
|
||||
}
|
||||
}
|
||||
+4
-2
@@ -3,8 +3,10 @@
|
||||
"allowSyntheticDefaultImports": true,
|
||||
"noImplicitAny": true,
|
||||
"module": "es6",
|
||||
"target": "es5",
|
||||
"allowJs": true
|
||||
"target": "es2017",
|
||||
"allowJs": true,
|
||||
"moduleResolution": "node",
|
||||
"typeRoots": ["./node_modules/@types", "./types"]
|
||||
},
|
||||
"files": ["src/main.ts"]
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
const path = require('path');
|
||||
const webpack = require('webpack');
|
||||
|
||||
const rootDir = path.resolve(__dirname);
|
||||
const date = (new Date()).toLocaleDateString("ru-RU", 'yyyy-MM-dd');
|
||||
const pckg = require(path.join(rootDir, 'package.json'));
|
||||
|
||||
const bannerContent = `
|
||||
${pckg.name} ${pckg.version} (${date})
|
||||
${pckg.homepage}
|
||||
${pckg.description}
|
||||
@author ${pckg.author.name}
|
||||
@license ${pckg.license}
|
||||
`.trim();
|
||||
|
||||
const libBanner = new webpack.BannerPlugin({
|
||||
banner: bannerContent,
|
||||
test: /\.js$/
|
||||
});
|
||||
|
||||
module.exports = { libBanner };
|
||||
+39
-29
@@ -1,42 +1,68 @@
|
||||
// Generated using webpack-cli https://github.com/webpack/webpack-cli
|
||||
|
||||
const path = require("path");
|
||||
const webpack = require("webpack");
|
||||
const BomPlugin = require('webpack-utf8-bom');
|
||||
const BomPlugin = require("webpack-utf8-bom");
|
||||
const TerserPlugin = require("terser-webpack-plugin");
|
||||
const { libBanner } = require("./webpack.banner");
|
||||
const { libUpload } = require("./webpack.upload");
|
||||
|
||||
const isProduction = process.env.NODE_ENV == "production";
|
||||
const isProduction = process.env.NODE_ENV === "production";
|
||||
|
||||
const config = {
|
||||
entry: "./src/main.ts",
|
||||
output: {
|
||||
path: path.resolve(__dirname, "dist"),
|
||||
filename: () => ('april-fools' + (isProduction ? '.min' : '') + '.js'),
|
||||
filename: () => "april-fools" + (isProduction ? ".min" : "") + ".js",
|
||||
},
|
||||
plugins: [
|
||||
new BomPlugin(true)
|
||||
// Add your plugins here
|
||||
// Learn more about plugins from https://webpack.js.org/configuration/plugins/
|
||||
new BomPlugin(true),
|
||||
libBanner,
|
||||
libUpload,
|
||||
],
|
||||
module: {
|
||||
rules: [
|
||||
{
|
||||
test: /\.(ts|tsx)$/i,
|
||||
test: /\.tsx?$/i,
|
||||
loader: "ts-loader",
|
||||
exclude: ["/node_modules/"],
|
||||
exclude: /node_modules|\.d\.ts$/,
|
||||
// exclude: ["/node_modules/|.d.ts"],
|
||||
options: {
|
||||
compilerOptions: {
|
||||
noEmit: false,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
test: /\.d\.ts$/i,
|
||||
loader: 'ignore-loader'
|
||||
},
|
||||
{
|
||||
test: /\.(eot|svg|ttf|woff|woff2|png|jpg|gif)$/i,
|
||||
type: "asset",
|
||||
},
|
||||
// Add your rules for custom modules here
|
||||
// Learn more about loaders from https://webpack.js.org/loaders/
|
||||
{
|
||||
test: /\.html$/i,
|
||||
loader: "html-loader",
|
||||
},
|
||||
],
|
||||
},
|
||||
resolve: {
|
||||
extensions: [".tsx", ".ts", ".jsx", ".js", "..."],
|
||||
},
|
||||
externals: {
|
||||
jquery: 'jQuery',
|
||||
jquery: "jQuery",
|
||||
},
|
||||
watchOptions: {
|
||||
aggregateTimeout: 20,
|
||||
},
|
||||
optimization: {
|
||||
minimize: isProduction,
|
||||
minimizer: [new TerserPlugin({
|
||||
terserOptions: {
|
||||
compress: true,
|
||||
},
|
||||
extractComments: false,
|
||||
minify: TerserPlugin.uglifyJsMinify,
|
||||
})],
|
||||
},
|
||||
};
|
||||
|
||||
@@ -48,19 +74,3 @@ module.exports = () => {
|
||||
}
|
||||
return config;
|
||||
};
|
||||
|
||||
/*
|
||||
{
|
||||
"compilerOptions": {
|
||||
"module": "AMD",
|
||||
"lib": ["es2017", "dom"],
|
||||
"rootDir": "./src/",
|
||||
"outFile": "dist/april-fools.js",
|
||||
"moduleResolution": "nodenext",
|
||||
"esModuleInterop": true
|
||||
},
|
||||
"files": [
|
||||
"src/main.ts"
|
||||
]
|
||||
}
|
||||
*/
|
||||
@@ -0,0 +1,16 @@
|
||||
|
||||
const { exec } = require('node:child_process');
|
||||
|
||||
const libUpload = {
|
||||
apply: (compiler) => {
|
||||
compiler.hooks.afterEmit.tap('AfterEmitPlugin', (compilation) => {
|
||||
exec('./upload.sh', (err, stdout, stderr) => {
|
||||
if (stdout) process.stdout.write(stdout);
|
||||
if (stderr) process.stderr.write(stderr);
|
||||
console.log('Uploaded.');
|
||||
});
|
||||
});
|
||||
},
|
||||
}
|
||||
|
||||
module.exports = { libUpload };
|
||||
Reference in New Issue
Block a user