Compare commits

...
13 Commits
39 changed files with 7785 additions and 2321 deletions
+4
View File
@@ -0,0 +1,4 @@
{
"presets": ["@babel/preset-env", "@babel/preset-typescript"],
"plugins": ["@babel/plugin-transform-optional-chaining"]
}
+27
View File
@@ -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"
}
}
+17
View File
@@ -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) обязуются соблюдать свои же условия.
+21 -17
View File
@@ -1,4 +1,4 @@
# Бибилотека первоапрельских розыгрышей для форумов MyBB # Библиотека первоапрельских розыгрышей для форумов MyBB
Базовое использование: Базовое использование:
@@ -26,6 +26,8 @@
- `letter_chaos` — **Буквенный хаос**. Все буквы в словах, кроме первой и последней, перемешены. - `letter_chaos` — **Буквенный хаос**. Все буквы в словах, кроме первой и последней, перемешены.
- `cursor` — **Красивые курсоры**. Эффекты, следующие за курсором. - `cursor` — **Красивые курсоры**. Эффекты, следующие за курсором.
- `stupid_title` — **Глупые статусы**. Каждому пользователю даётся новый глупый статус. - `stupid_title` — **Глупые статусы**. Каждому пользователю даётся новый глупый статус.
- `inverted_scroll` — **Обратная гравитация**. Скролл в обратную сторону.
- `latinization` — **Латинизация**. Все символы на странице или только в постах или еще где-то местами - латиницей вместо кириллицы и наоборот
--- ---
@@ -48,9 +50,9 @@ window.AprilJokes.settings = {
## Настройка шуток ## Настройка шуток
Настройка шуток происходит при присвоении объекта `window.AprilJokes.settings`. Настройка шуток происходит при присвоении объекта `window.AprilJokes.settings`.
Это присвоедние должно произойти **после** загрузки скрипта. Это присвоение должно произойти **после** загрузки скрипта.
Для всех шуток определны 2 общих поля: Для всех шуток определены 2 общих поля:
```js ```js
window.AprilJokes.settings = { window.AprilJokes.settings = {
@@ -75,7 +77,7 @@ window.AprilJokes.settings = {
}; };
``` ```
Полный список настроек с объяснениями вы можете посмотреть в файле [`settings.example.js`](./settings.example.js). Также представлен файл [`settings.compact.js`](./settings.compact.js), который также содержит все настройки, но без коментариев. Полный список настроек с объяснениями вы можете посмотреть в файле [`settings.example.js`](./settings.example.js). Также представлен файл [`settings.compact.js`](./settings.compact.js), который также содержит все настройки, но без комментариев.
При этом, объективно, вставлять весь этот код в HTML-начало может быть "немного" неудобно, поэтому вы можете скачать код (кнопка "Raw" над файлом), изменить его, и загрузить в файлы форума, подключив скрипт таким образом: При этом, объективно, вставлять весь этот код в HTML-начало может быть "немного" неудобно, поэтому вы можете скачать код (кнопка "Raw" над файлом), изменить его, и загрузить в файлы форума, подключив скрипт таким образом:
@@ -88,7 +90,7 @@ window.AprilJokes.settings = {
Заметьте, что во всех примерах приведены значения настроек по умолчанию. Вы _можете_ удалять строки, значения по умолчанию которых вас устраивают. Заметьте, что во всех примерах приведены значения настроек по умолчанию. Вы _можете_ удалять строки, значения по умолчанию которых вас устраивают.
Если вас интерсует только несколько шуток, вы можете использовать метод `disableAll()`, что бы выключить выполнение всех шуток, а потом включать отдельные шутки через установку настройки `enabled`: Если вас интересует только несколько шуток, вы можете использовать метод `disableAll()`, что бы выключить выполнение всех шуток, а потом включать отдельные шутки через установку настройки `enabled`:
```js ```js
window.AprilJokes.disableAll(); window.AprilJokes.disableAll();
@@ -127,7 +129,7 @@ window.AprilJokes.settings = {
window.AprilJokes.start("mirror", true); window.AprilJokes.start("mirror", true);
``` ```
Так же можете [посмотреть](./src/jokes/mirror.ts#L16) на метод `MirrorJoke.start`, что бы узнать как именно проиходит отражение. Так же можете [посмотреть](./src/jokes/mirror.ts#L16) на метод `MirrorJoke.start`, что бы узнать как именно происходит отражение.
--- ---
@@ -147,9 +149,9 @@ window.AprilJokes.jokes.cursor.disableAll();
### Настройка `stupid_title` (Глупые статусы) ### Настройка `stupid_title` (Глупые статусы)
> **Важно**: этот блок может быть чрезвычайно капризым из-за особенностей хранения данных, особенно блок `variants`. Изменяя его, строго следуйте этой инструкции, иначе скрипт может перестать работать ожидаемым образом. > **Важно**: этот блок может быть чрезвычайно капризным из-за особенностей хранения данных, особенно блок `variants`. Изменяя его, строго следуйте этой инструкции, иначе скрипт может перестать работать ожидаемым образом.
Данная шутка генерирет статус из случайных слов. За счёт использования псевдослучайного генератора, хеширования и привязки к времени UTC, ожидается что все пользователи одновременно будут видеть один и тот же статус для каждого пользователя, при этом он меняется каждые `change_frequency` минут. Установите это значение равным `0`, что бы статус генерировался при каждом обновлении страницы. Данная шутка генерирует статус из случайных слов. За счёт использования псевдослучайного генератора, хеширования и привязки к времени UTC, ожидается что все пользователи одновременно будут видеть один и тот же статус для каждого пользователя, при этом он меняется каждые `change_frequency` минут. Установите это значение равным `0`, что бы статус генерировался при каждом обновлении страницы.
Для настройки списка слов используется функция `BuildStupdiTitleVariant`, которая преобразует список слов во внутренний формат скрипта. Настоятельно рекомендуется использовать именно её, а не записывать параметры напрямую: Для настройки списка слов используется функция `BuildStupdiTitleVariant`, которая преобразует список слов во внутренний формат скрипта. Настоятельно рекомендуется использовать именно её, а не записывать параметры напрямую:
@@ -161,14 +163,14 @@ window.AprilJokes.settings = {
variants: { variants: {
adj: { adj: {
...window.AprilJokes.settings.stupid_title.variants.adj, ...window.AprilJokes.settings.stupid_title.variants.adj,
...BuildStupdiTitleVariant(true, "скромный, шутливый, !грубый"), ...BuildStupidTitleVariant(true, "скромный, шутливый, !грубый"),
...BuildStupdiTitleVariant(false, "скромная, шутливая, !грубая"), ...BuildStupidTitleVariant(false, "скромная, шутливая, !грубая"),
}, },
noun: { ...window.AprilJokes.settings.stupid_title.variants.noun }, noun: {...window.AprilJokes.settings.stupid_title.variants.noun},
verb: { ...window.AprilJokes.settings.stupid_title.variants.verb }, verb: {...window.AprilJokes.settings.stupid_title.variants.verb},
place: { ...window.AprilJokes.settings.stupid_title.variants.place }, place: {...window.AprilJokes.settings.stupid_title.variants.place},
adv: { ...window.AprilJokes.settings.stupid_title.variants.adv }, adv: {...window.AprilJokes.settings.stupid_title.variants.adv},
p: { ...window.AprilJokes.settings.stupid_title.variants.p }, p: {...window.AprilJokes.settings.stupid_title.variants.p},
}, },
// ... // ...
}, },
@@ -178,12 +180,12 @@ window.AprilJokes.settings = {
Обратите внимание на конструкции вида `{ ...something }` — эта конструкция позволяет перенести все поля нескольких старых объектов в один новый. При этом, если в двух объединяемых объектах есть совпадающие значения, то будет сохранено последнее. Обратите внимание на конструкции вида `{ ...something }` — эта конструкция позволяет перенести все поля нескольких старых объектов в один новый. При этом, если в двух объединяемых объектах есть совпадающие значения, то будет сохранено последнее.
Говоря проще: добовляйте ваши настройки _после_ строки вида `...window.AprilJokes.settings.stupid_title.variants.adj`, что бы ваши настройки не были перезаписаны. Говоря проще: добавляйте ваши настройки _после_ строки вида `...window.AprilJokes.settings.stupid_title.variants.adj`, что бы ваши настройки не были перезаписаны.
Метод `BuildStupdiTitleVariant` принимает 2 аргумента: Метод `BuildStupdiTitleVariant` принимает 2 аргумента:
- `gender` — определённый род для генерации. Используйте `true` для мужского рода, `false` для женского и `null`, если добавляемые варианты могут использоваться в обоих случаях. - `gender` — определённый род для генерации. Используйте `true` для мужского рода, `false` для женского и `null`, если добавляемые варианты могут использоваться в обоих случаях.
- `words` — строка, со списоком вариантов через запятую. В качестве вариантов могут быть слова из русских или английских букв, пробелов или дефисов, то есть можно использовать `"..., почему-то и зачем-то , ..."` и `почему-то и зачем-то` будет воспринят как один вариант. Также варианты можно модифицировать, добавив один или несколько модификаторов: - `words` — строка, со списком вариантов через запятую. В качестве вариантов могут быть слова из русских или английских букв, пробелов или дефисов, то есть можно использовать `"..., почему-то и зачем-то , ..."` и `почему-то и зачем-то` будет воспринят как один вариант. Также варианты можно модифицировать, добавив один или несколько модификаторов:
- `!` отключает использование слова. Этим модификатором можно отключать слова из списка по умолчанию (см `!грубый` в примере выше). - `!` отключает использование слова. Этим модификатором можно отключать слова из списка по умолчанию (см `!грубый` в примере выше).
- `%` помечает слово как редкое. Шанс его использования понижается в 2 раза. - `%` помечает слово как редкое. Шанс его использования понижается в 2 раза.
- `^` помечает слово как частое. Шанс его использования повышается в 2 раза. - `^` помечает слово как частое. Шанс его использования повышается в 2 раза.
@@ -218,6 +220,8 @@ window.AprilJokes.settings = {
// Стандартные настройки // Стандартные настройки
{ {
title: 'Название шутки',
description: 'Описание шутки',
enabled: true, enabled: true,
chance: 30, chance: 30,
text: "Fooled!", text: "Fooled!",
+124 -24
View File
File diff suppressed because one or more lines are too long
+7 -1
View File
File diff suppressed because one or more lines are too long
+5205 -1718
View File
File diff suppressed because it is too large Load Diff
+18 -6
View File
@@ -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": { "devDependencies": {
"@babel/cli": "^7.21.0", "@babel/cli": "^7.21.0",
"@babel/core": "^7.21.3", "@babel/core": "^7.24.3",
"@babel/preset-env": "^7.20.2", "@babel/preset-env": "^7.20.2",
"@babel/preset-typescript": "^7.21.0", "@babel/preset-typescript": "^7.21.0",
"@types/jquery": "^3.5.16", "@types/jquery": "^3.5.16",
@@ -9,6 +19,8 @@
"@types/node": "^18.15.10", "@types/node": "^18.15.10",
"@webpack-cli/generators": "^3.0.1", "@webpack-cli/generators": "^3.0.1",
"babelify": "^10.0.0", "babelify": "^10.0.0",
"eslint": "^8.57.0",
"eslint-config-google": "^0.14.0",
"grunt": "^1.6.1", "grunt": "^1.6.1",
"grunt-browserify": "^6.0.0", "grunt-browserify": "^6.0.0",
"grunt-contrib-uglify": "^5.2.2", "grunt-contrib-uglify": "^5.2.2",
@@ -19,7 +31,7 @@
"markdown-it": "^13.0.1", "markdown-it": "^13.0.1",
"markdown-it-container": "^3.0.0", "markdown-it-container": "^3.0.0",
"prettier": "^2.8.2", "prettier": "^2.8.2",
"terser-webpack-plugin": "^5.3.7", "terser-webpack-plugin": "^5.3.10",
"ts-loader": "^9.4.2", "ts-loader": "^9.4.2",
"tsify": "^5.0.4", "tsify": "^5.0.4",
"typescript": "^5.0.2", "typescript": "^5.0.2",
@@ -34,11 +46,11 @@
"watch": "webpack --watch" "watch": "webpack --watch"
}, },
"dependencies": { "dependencies": {
"@babel/eslint-parser": "^7.24.1",
"@typescript-eslint/eslint-plugin": "^7.3.1",
"cursor-effects": "^1.0.11", "cursor-effects": "^1.0.11",
"ignore-loader": "^0.1.2",
"jquery": "^3.6.4", "jquery": "^3.6.4",
"webpack-utf8-bom": "^1.4.0" "webpack-utf8-bom": "^1.4.0"
}, }
"version": "1.0.0",
"description": "Mybb April Fools",
"name": "mybb-april-fools"
} }
+50
View File
@@ -3,6 +3,11 @@ window.AprilJokes.disableAll();
window.AprilJokes.jokes.cursor.disableAll(); window.AprilJokes.jokes.cursor.disableAll();
window.AprilJokes.settings = { window.AprilJokes.settings = {
core: {
enabled: true,
chance: 100,
},
shake: { shake: {
enabled: true, enabled: true,
chance: 1, chance: 1,
@@ -157,4 +162,49 @@ window.AprilJokes.settings = {
p: { ...window.AprilJokes.settings.stupid_title.variants.p }, 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,
}
}; };
+127 -7
View File
@@ -14,17 +14,17 @@ window.AprilJokes.settings = {
*/ */
core: { core: {
/** /**
* Активность шутки. * Активность системы.
* Установите это значение `false`, что бы отключить шутку. * Установите это значение `false`, что бы отключить всю системы.
* Если `true`, то шутка будет обработана как обычно. * Если `true`, то система будет обработана как обычно.
*/ */
enabled: true, enabled: true,
/** /**
* Вероятность срабатывания в процентах. * Вероятность срабатывания всей системы в процентах.
* Если установлено значение `100`, шутка будет срабатывать всегда. * Если установлено значение `100`, система будет срабатывать всегда.
* Если установлено значение `0`, шутка никогда не сработает. * Если установлено значение `0`, система никогда не сработает.
* Лучше отключать шутку, используя enabled = false! * Лучше отключать систему, используя `enabled = false`!
*/ */
chance: 100, chance: 100,
}, },
@@ -450,4 +450,124 @@ window.AprilJokes.settings = {
p: { ...window.AprilJokes.settings.stupid_title.variants.p }, 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,
}
}; };
+100 -8
View File
@@ -1,14 +1,106 @@
import $ from "jquery"; import $ from 'jquery';
/** /**
* Получение всех элементов, являющимися TextNode. * Получение всех элементов, являющимися TextNode.
* Используется некоторыми шутками для уменьшения влияния на существующее DOM-дерево. * Используется некоторыми шутками для уменьшения
* влияния на существующее DOM-дерево.
*
* @param {HTMLElement | JQuery<HTMLElement> | JQuery.Selector} root
* Элемет, у которого необходимо найти текстовые ноды
*
* @return {JQuery<HTMLElement | Text | Comment | Document>}
*/ */
export function getTextNodes( export function getTextNodes(
root: any root: HTMLElement | JQuery<HTMLElement> | JQuery.Selector,
): JQuery<HTMLElement | Text | Comment | Document> { )
return $(root) : JQuery<HTMLElement | Text | Comment | Document> {
.find("*") // Это очень странный код, но надо успокоить ESLint
.contents() const $root = (typeof root === 'string') ? $(root) : $(root);
.filter((_, node) => node.nodeType === Node.TEXT_NODE);
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;
} }
+34 -20
View File
@@ -1,56 +1,70 @@
import { import {
StupidTitleVariants, StupidTitleVariants,
BuildStupdiTitleVariant, BuildStupidTitleVariant,
} from "../jokes/stupid_title"; } from '../jokes/stupid_title';
/**
* Метод, генерирующий стандартный список слов
* @param {StupidTitleVariants} vars
*/
export function StupidTitleDefaultVariants(vars: StupidTitleVariants) { export function StupidTitleDefaultVariants(vars: StupidTitleVariants) {
vars.adj = { vars.adj = {
...BuildStupdiTitleVariant( ...BuildStupidTitleVariant(
true, true,
"адекватный,$адский,активный,алчный,%ангельский,банальный,безнадёжный,$бесполезный,былой,верный,виртуальный,вкусный,воспитанный,выгодный,выходной,гигантский,$глупый,гордый,грамотный,громадный,грубый,далекий,двуличный,деятельный,доброжелательный,$жалкий,жёсткий,жестокий,$жирный,заинтересованный,запасной,застенчивый,знакомый,иностранный,истинный,конструктивный,креативный,ласковый,любимый,мелочный,^мирный,мстительный,небесный,невозможный,независимый,низкий,нужный,объективный,обыкновенный,опрятный,^оригинальный,особенный,острый,откровенный,открытый,пестрый,повседневный,подземный,пожилой,показательный,последний,посторонний,провинциальный,пышный,робкий,родной,самовлюблённый,северный,седой,семейный,серьёзный,сногсшибательный,^солнечный,спокойный,суровый,терпеливый,уверенный в себе,ударный,^удачливый,формальный,хладнокровный,целый,чистый,чрезвычайный,чужой,южный" // eslint-disable-next-line max-len
'адекватный,$адский,активный,алчный,%ангельский,банальный,безнадёжный,$бесполезный,былой,верный,виртуальный,вкусный,воспитанный,выгодный,выходной,гигантский,$глупый,гордый,грамотный,громадный,грубый,далекий,двуличный,деятельный,доброжелательный,$жалкий,жёсткий,жестокий,$жирный,заинтересованный,запасной,застенчивый,знакомый,иностранный,истинный,конструктивный,креативный,ласковый,любимый,мелочный,^мирный,мстительный,небесный,невозможный,независимый,низкий,нужный,объективный,обыкновенный,опрятный,^оригинальный,особенный,острый,откровенный,открытый,пестрый,повседневный,подземный,пожилой,показательный,последний,посторонний,провинциальный,пышный,робкий,родной,самовлюблённый,северный,седой,семейный,серьёзный,сногсшибательный,^солнечный,спокойный,суровый,терпеливый,уверенный в себе,ударный,^удачливый,формальный,хладнокровный,целый,чистый,чрезвычайный,чужой,южный'
), ),
...BuildStupdiTitleVariant( ...BuildStupidTitleVariant(
false, false,
"адекватная,$адская,активная,алчная,%ангельская,банальная,безнадёжная,$бесполезная,былая,верная,виртуальная,вкусная,воспитанная,выгодная,выходная,гигантская,$глупая,гордая,грамотная,громадная,грубая,далекая,двуличная,деятельная,доброжелательная,$жалкая,жёсткая,жестокая,$жирная,заинтересованная,запасная,застенчивая,знакомая,иностранная,истинная,конструктивная,креативная,ласковая,любимая,мелочная,^мирная,мстительная,небесная,невозможная,независимая,низкая,нужная,объективная,обыкновенная,опрятная,^оригинальная,особенная,острая,откровенная,открытая,пестрая,повседневная,подземная,пожилая,показательная,последняя,посторонняя,провинциальная,пышная,робкая,родная,самовлюблённая,северная,седая,семейная,серьезная,сногсшибательная,^солнечная,спокаяная,суровая,терпеливая,уверенная в себе,ударная,^удачливая,формальная,хладнокровная,целая,чистая,чрезвычайная,чужая,южная" // eslint-disable-next-line max-len
'адекватная,$адская,активная,алчная,%ангельская,банальная,безнадёжная,$бесполезная,былая,верная,виртуальная,вкусная,воспитанная,выгодная,выходная,гигантская,$глупая,гордая,грамотная,громадная,грубая,далекая,двуличная,деятельная,доброжелательная,$жалкая,жёсткая,жестокая,$жирная,заинтересованная,запасная,застенчивая,знакомая,иностранная,истинная,конструктивная,креативная,ласковая,любимая,мелочная,^мирная,мстительная,небесная,невозможная,независимая,низкая,нужная,объективная,обыкновенная,опрятная,^оригинальная,особенная,острая,откровенная,открытая,пестрая,повседневная,подземная,пожилая,показательная,последняя,посторонняя,провинциальная,пышная,робкая,родная,самовлюблённая,северная,седая,семейная,серьезная,сногсшибательная,^солнечная,спокаяная,суровая,терпеливая,уверенная в себе,ударная,^удачливая,формальная,хладнокровная,целая,чистая,чрезвычайная,чужая,южная'
), ),
...vars.adj, ...vars.adj,
}; };
vars.noun = { vars.noun = {
...BuildStupdiTitleVariant( ...BuildStupidTitleVariant(
true, true,
"актёр,$балбес,бард,басурманин,борец,бродяга,бруталище,волк,дворф,%дед,%дедушка,джентльмен,диван,динозавр,дракон,ёж,%ёжик,жук,зайц,$злоумышленник,зомби,карасик,кентавр,кит,кот,кролик,кулак,лев,ледник,лесоруб,%мальчег,мальчик,манекен,массажист,мачо,монах,монстр,наследник,ниндзя,носорог,одиночка,организм,орёл,отец,принц,проказник,пузырь,$раб,рыцарь,спортсмен,сурикат,сын,%сынок,тигр,товарищ,хоббит,эльф" // eslint-disable-next-line max-len
'актёр,$балбес,бард,басурманин,борец,бродяга,бруталище,волк,дворф,%дед,%дедушка,джентльмен,диван,динозавр,дракон,ёж,%ёжик,жук,зайц,$злоумышленник,зомби,карасик,кентавр,кит,кот,кролик,кулак,лев,ледник,лесоруб,%мальчег,мальчик,манекен,массажист,мачо,монах,монстр,наследник,ниндзя,носорог,одиночка,организм,орёл,отец,принц,проказник,пузырь,$раб,рыцарь,спортсмен,сурикат,сын,%сынок,тигр,товарищ,хоббит,эльф'
), ),
...BuildStupdiTitleVariant( ...BuildStupidTitleVariant(
false, false,
"актриса,акула,$бабка,бабочка,%бабушка,бактерия,белочка,бусинка,валькирия,$ведьма,волторна,волчица,горгулья,дворфиха,девочка,деревяшка,детка,%деффачка,%дочка,дочь,заноза,$злоумышленница,знаменитость,каруселька,килька,конфетка,косточка,кошка,красотка,крышка,леди,$личинка,львица,мать,машина,милашка,монашка,мышь,наследница,орлица,панда,пиранья,принцесса,простуда,$рабыня,русалочка,рыбка,рыбка,скамеечка,спортсменка,тигрица,травка,черепашка,чертовка,эльфийка" // eslint-disable-next-line max-len
'актриса,акула,$бабка,бабочка,%бабушка,бактерия,белочка,бусинка,валькирия,$ведьма,волторна,волчица,горгулья,дворфиха,девочка,деревяшка,детка,%деффачка,%дочка,дочь,заноза,$злоумышленница,знаменитость,каруселька,килька,конфетка,косточка,кошка,красотка,крышка,леди,$личинка,львица,мать,машина,милашка,монашка,мышь,наследница,орлица,панда,пиранья,принцесса,простуда,$рабыня,русалочка,рыбка,рыбка,скамеечка,спортсменка,тигрица,травка,черепашка,чертовка,эльфийка'
), ),
...vars.noun, ...vars.noun,
}; };
vars.verb = { vars.verb = {
...BuildStupdiTitleVariant( ...BuildStupidTitleVariant(
null, null,
"бежит,берёт и делает,болеет,встаёт,говорит,думает,забывает,замирает,заходит,идёт,%имеет план и придерживается его,ловит,меняется,молчит,нарушает,оглядывается,орёт,освобождает,отвечает,отказаться куда-то идти,отказывается,отказывается вставать,отказывается что-то делать,оформляет,плывёт,поглядывает,поздравляет,поздравляет,получает,прибывает,приказывает,просыпается,прощает,работает,радуется,скрывается,снимает,снимает,снимается в сериале,снимается в фильме,собирается,соединяет,спрашивает,тянет,указывает,учит,читает,что-то скрывает,чувствует опасность,шепчет" // eslint-disable-next-line max-len
'бежит,берёт и делает,болеет,встаёт,говорит,думает,забывает,замирает,заходит,идёт,%имеет план и придерживается его,ловит,меняется,молчит,нарушает,оглядывается,орёт,освобождает,отвечает,отказаться куда-то идти,отказывается,отказывается вставать,отказывается что-то делать,оформляет,плывёт,поглядывает,поздравляет,поздравляет,получает,прибывает,приказывает,просыпается,прощает,работает,радуется,скрывается,снимает,снимает,снимается в сериале,снимается в фильме,собирается,соединяет,спрашивает,тянет,указывает,учит,читает,что-то скрывает,чувствует опасность,шепчет'
), ),
...BuildStupdiTitleVariant( ...BuildStupidTitleVariant(
true, true,
"взял и сделал,додумался,женился,женится,заработался,не спал,не успел,обиделся,познал дзен,присел,проснулся,скрылся,смог,успел" // eslint-disable-next-line max-len
'взял и сделал,додумался,женился,женится,заработался,не спал,не успел,обиделся,познал дзен,присел,проснулся,скрылся,смог,успел'
), ),
...BuildStupdiTitleVariant( ...BuildStupidTitleVariant(
false, false,
"взяла и сделала,выходит замуж,вышла замуж,додумалась,заработалась,не спала,не успела,обиделась,познала дзен,присела,проснулась,скрылась,смогла,успела" // eslint-disable-next-line max-len
'взяла и сделала,выходит замуж,вышла замуж,додумалась,заработалась,не спала,не успела,обиделась,познала дзен,присела,проснулась,скрылась,смогла,успела'
), ),
...vars.verb, ...vars.verb,
}; };
vars.place = { vars.place = {
...BuildStupdiTitleVariant( ...BuildStupidTitleVariant(
null, null,
"в джунглях,в квартире,в космосе,в лесу,в очереди,в пустыне,в театре,%в тёплой и уютной квартире,в филармонии,где-то,на заснеженной вершине,на конференции,на пляже,на форуме,под водой,под дождём,%под проливным дождём" // eslint-disable-next-line max-len
'в джунглях,в квартире,в космосе,в лесу,в очереди,в пустыне,в театре,%в тёплой и уютной квартире,в филармонии,где-то,на заснеженной вершине,на конференции,на пляже,на форуме,под водой,под дождём,%под проливным дождём'
), ),
...vars.place, ...vars.place,
}; };
vars.adv = { vars.adv = {
...BuildStupdiTitleVariant( ...BuildStupidTitleVariant(
null, null,
"безрассудно,быстро,внезапно,вновь,$грубо,грустно,медленно,%молниеносно,немедленно,опять,осмысленно,позже,предсказуемо,расчётливо,регулярно,счастливо,уже,уныло" // eslint-disable-next-line max-len
'безрассудно,быстро,внезапно,вновь,$грубо,грустно,медленно,%молниеносно,немедленно,опять,осмысленно,позже,предсказуемо,расчётливо,регулярно,счастливо,уже,уныло'
), ),
...vars.adv, ...vars.adv,
}; };
+16
View File
@@ -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>
+64 -1
View File
@@ -11,15 +11,19 @@
transition: 0.4s all; transition: 0.4s all;
cursor: pointer; cursor: pointer;
} }
.april-fools__button:hover { .april-fools__button:hover {
opacity: 1; opacity: 1;
} }
.april-fools__joke-row { .april-fools__joke-row {
border-bottom: 1px solid black; border-bottom: 1px solid black;
} }
.april-fools__joke-row:first-of-type { .april-fools__joke-row:first-of-type {
border-top: 1px solid black; border-top: 1px solid black;
} }
.april-fools__joke { .april-fools__joke {
display: flex; display: flex;
flex-flow: row wrap; flex-flow: row wrap;
@@ -30,37 +34,96 @@
gap: 1ch; gap: 1ch;
cursor: pointer; cursor: pointer;
} }
.april-fools__joke:hover { .april-fools__joke:hover {
text-shadow: 0 0 black; text-shadow: 0 0 black;
} }
.april-fools__joke:before { .april-fools__joke:before {
display: block; display: block;
content: "";
flex: 0 0 2ch; flex: 0 0 2ch;
text-align: center; text-align: center;
content: "✔"; content: "✔";
opacity: 0; opacity: 0;
} }
input.april-fools__joke-input:checked + .april-fools__joke { input.april-fools__joke-input:checked + .april-fools__joke {
opacity: 1; opacity: 1;
} }
input.april-fools__joke-input:checked + .april-fools__joke:before { input.april-fools__joke-input:checked + .april-fools__joke:before {
opacity: 1; opacity: 1;
} }
.april-fools__joke .april-fools__joke-title { .april-fools__joke .april-fools__joke-title {
flex: 1 auto; flex: 1 auto;
font-weight: bold; font-weight: bold;
} }
.april-fools__joke .april-fools__joke-description { .april-fools__joke .april-fools__joke-description {
flex: 1 auto; flex: 1 auto;
text-align: right; text-align: right;
} }
.april-fools__joke .april-fools__joke-chance { .april-fools__joke .april-fools__joke-chance {
flex: 0 0 4ch; flex: 0 0 4ch;
text-align: right; text-align: right;
} }
.april-fools__content .april-fools__reset-button { .april-fools__content .april-fools__reset-button {
display: block; display: block;
margin: 10px auto; 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> </style>
+3 -3
View File
@@ -15,9 +15,9 @@
style="display: none" style="display: none"
/> />
<label class="april-fools__joke" for="april-fools__joke-${joke.id}"> <label class="april-fools__joke" for="april-fools__joke-${joke.id}">
<p class="april-fools__joke-title">${joke.title}</p> <span class="april-fools__joke-title">${joke.title}</span>
<p class="april-fools__joke-description">${joke.description}</p> <span class="april-fools__joke-description">${joke.description}</span>
<p class="april-fools__joke-chance">${joke.settings.chance}%</p> <span class="april-fools__joke-chance">${joke.settings.chance}%</span>
</label> </label>
</div> </div>
</template> </template>
+42 -8
View File
@@ -1,22 +1,22 @@
/** /**
* Объект настроек шутки * Объект настроек шутки
*/ */
export interface JokeSettings { export class JokeSettings {
/** /**
* Флаг блокировки срабатывания * Флаг блокировки срабатывания
*/ */
enabled: boolean; enabled: boolean = true;
/** /**
* Вероятность срабатывания в процентах * Вероятность срабатывания в процентах
*/ */
chance: number; chance: number = 20;
} }
/** /**
* Объект, описывающий шутку * Объект, описывающий шутку
*/ */
export abstract class Joke { export abstract class Joke<T extends JokeSettings> {
/** /**
* Идентификатор шутки * Идентификатор шутки
*/ */
@@ -35,13 +35,47 @@ export abstract class Joke {
/** /**
* Настройки шутки * Настройки шутки
*/ */
settings: JokeSettings; _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 Вероятность срабатывания * @param {number} chance Вероятность срабатывания
* @returns {boolean} Флаг срабатывания (true - сработало) * @return {boolean} Флаг срабатывания (true - сработало)
*/ */
check(chance: number): boolean { check(chance: number): boolean {
return Math.random() < 0.01 * chance; return Math.random() < 0.01 * chance;
@@ -63,7 +97,7 @@ export abstract class Joke {
/** /**
* Переключить блокировку шутки * Переключить блокировку шутки
* @param value Новое значение * @param {boolean | null} [value] Новое значение
*/ */
toggle(value: boolean | null = null) { toggle(value: boolean | null = null) {
this.settings.enabled = value === null ? !this.settings.enabled : value; this.settings.enabled = value === null ? !this.settings.enabled : value;
@@ -78,7 +112,7 @@ export abstract class Joke {
/** /**
* Объект, описывающий шутку, которая может быть остановлена * Объект, описывающий шутку, которая может быть остановлена
*/ */
export abstract class StopableJoke extends Joke { export abstract class StoppableJoke<T extends JokeSettings> extends Joke<T> {
/** /**
* Остановка шутки, если возможно * Остановка шутки, если возможно
*/ */
+126 -99
View File
@@ -1,6 +1,7 @@
import { stat } from "fs"; import './types/mybb.d';
import { Joke, JokeSettings, StopableJoke } from "./joke"; import {Joke, JokeSettings, StoppableJoke} from './joke';
import { JokerUI } from "./ui"; import {JokerUI} from './ui';
import {debug} from './common';
/** /**
* Блок обработки перво-апрельских шуток * Блок обработки перво-апрельских шуток
@@ -9,58 +10,15 @@ import { JokerUI } from "./ui";
*/ */
export class JokerClass { export class JokerClass {
ui: JokerUI = new JokerUI(this); ui: JokerUI = new JokerUI(this);
/**
* Общие настройки системы
*/
core_settings = new CoreSettings();
/** /**
* Проверка вероятности * Перечисление всех шуток
*
* @param {number} chance Вероятность срабатывания
* @returns {boolean} Флаг срабатывания (true - сработало)
*/ */
check(chance: number): boolean { jokes: JokeMap = {};
return Math.random() < 0.01 * chance;
}
/**
* Добавление шутки
*
* @param joke Объект шутки
*/
add(joke: Joke): void {
this.jokes[joke.id] = joke;
}
/**
* Регистрация новой шутки
*
* @param id Идентификатор шутки
* @param handler Метод, выполняющий шутку
* @param settings Объект настроек шутки
*/
addCustom(
id: string,
handler: Function,
settings: JokeSettings | null = null
): Joke {
const joke = new (class extends Joke {
id = id;
title = "Пользовательская шутка";
description = "Для шутки не определено описание";
settings: JokeSettings = { chance: 10, enabled: true };
start() {
handler.call(this, this.settings);
}
})();
Object.assign(joke.settings, settings ?? {});
this.add(joke);
return joke;
}
/** /**
* Получение полного списка настроек * Получение полного списка настроек
@@ -68,7 +26,7 @@ export class JokerClass {
get settings(): SettingsMap { get settings(): SettingsMap {
const result: SettingsMap = {}; const result: SettingsMap = {};
result["core"] = this.core_settings; result['core'] = this.core_settings;
for (const entries of Object.entries(this.jokes)) { for (const entries of Object.entries(this.jokes)) {
const id = entries[0]; const id = entries[0];
@@ -83,19 +41,75 @@ export class JokerClass {
/** /**
* Запись новых настроек * Запись новых настроек
* *
* @param settings * @param {SettingsMap} settings
*/ */
set settings(settings: SettingsMap) { set settings(settings: SettingsMap) {
for (const entry of Object.entries(settings)) { for (const entry of Object.entries(settings)) {
const id = entry[0]; const id = entry[0];
const jokeSettings = entry[1]; const jokeSettings = entry[1];
const targetSettings = const targetSettings =
id === "core" ? this.core_settings : this.jokes[id].settings; id === 'core' ? this.core_settings : this.jokes[id].settings;
Object.assign(targetSettings, jokeSettings); 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;
}
/** /**
* Разблокировать систему * Разблокировать систему
*/ */
@@ -130,12 +144,15 @@ export class JokerClass {
/** /**
* Переключение шутки * Переключение шутки
*
* @param {string} jokeId ID шутки для переключения
* @param {boolean} state Новое состояние
*/ */
toggleJoke(joke_id: string, state: boolean) { toggleJoke(jokeId: string, state: boolean) {
const joke = this.jokes[joke_id]; const joke = this.jokes[jokeId];
joke.settings.enabled = state; joke.settings.enabled = state;
if (joke instanceof StopableJoke && !state) { if (joke instanceof StoppableJoke && !state) {
joke.stop(); joke.stop();
} }
@@ -146,23 +163,23 @@ export class JokerClass {
* Сохранение пользовательских настроек * Сохранение пользовательских настроек
*/ */
saveUserStates() { saveUserStates() {
const states: any = {}; const states: { [key in string]: boolean } = {};
for (const key of Object.keys(this.jokes)) { for (const key of Object.keys(this.jokes)) {
states[key] = this.jokes[key].settings.enabled; states[key] = this.jokes[key].settings.enabled;
} }
fetch( fetch(
"/api.php?" + '/api.php?' +
$.param({ $.param({
method: "storage.set", method: 'storage.set',
user_id: (<any>window).UserID, user_id: window.UserID,
key: "april-fools", key: 'april-fools',
token: (<any>window).ForumAPITicket, token: window.ForumAPITicket,
value: JSON.stringify(states), value: JSON.stringify(states),
timer: 24 * 60, timer: 24 * 60,
}) }),
); ).then((_) => {});
} }
/** /**
@@ -170,12 +187,12 @@ export class JokerClass {
*/ */
async loadUserStates() { async loadUserStates() {
const response = await fetch( const response = await fetch(
"/api.php?" + '/api.php?' +
$.param({ $.param({
method: "storage.get", method: 'storage.get',
user_id: (<any>window).UserID, user_id: window.UserID,
key: "april-fools", key: 'april-fools',
}) }),
); );
const json = await response.json(); const json = await response.json();
@@ -183,22 +200,25 @@ export class JokerClass {
return; return;
} }
const data = JSON.parse(json.response.storage.data["april-fools"]); const data = JSON.parse(json.response.storage.data['april-fools']);
for (const key of Object.keys(this.jokes)) { for (const key of Object.keys(this.jokes)) {
this.jokes[key].settings.enabled = data[key]; this.jokes[key].settings.enabled = data[key];
} }
} }
/**
* Отчистка настроек пользователя
*/
async clearUserStates() { async clearUserStates() {
await fetch( await fetch(
"/api.php?" + '/api.php?' +
$.param({ $.param({
method: "storage.delete", method: 'storage.delete',
user_id: (<any>window).UserID, user_id: window.UserID,
key: "april-fools", key: 'april-fools',
token: (<any>window).ForumAPITicket, token: window.ForumAPITicket,
}) }),
); );
return; return;
@@ -207,26 +227,27 @@ export class JokerClass {
/** /**
* Запуск шутки * Запуск шутки
* *
* @param id * @param {string} jokeId ID шутки для запуска
* @param {boolean} forced * @param {boolean} [forced] Принудительный запуск
*/ */
start(id: string, forced: boolean = false) { start(jokeId: string, forced: boolean = false) {
if (!(id in this.jokes)) return; if (!(jokeId in this.jokes)) return;
const joke = this.jokes[id]; const joke = this.jokes[jokeId];
if ( if (!forced && (!joke.settings.enabled || !this.check(joke.settings.chance))
!forced &&
(!joke.settings.enabled || !this.check(joke.settings.chance))
) { ) {
debug(joke.title, '- шутка не включена');
return; return;
} }
debug(joke.title, '- шутка включена');
joke.start(); joke.start();
} }
/** /**
* Запуск всех шуток * Запуск всех шуток
*
* @param {boolean} [forced] Принудительный запуск
*/ */
startAll(forced: boolean = false): void { startAll(forced: boolean = false): void {
for (const key of Object.keys(this.jokes)) { for (const key of Object.keys(this.jokes)) {
@@ -234,12 +255,17 @@ export class JokerClass {
} }
} }
/**
* Стандартная проверка 1 апреля
*
* @return {boolean} Флаг, должна ли запускаться система
*/
regularCheck(): boolean { regularCheck(): boolean {
if (!this.core_settings.enabled) { if (!this.core_settings.enabled) {
return false; return false;
} }
if (this.core_settings.testers.indexOf((<any>window).UserID) >= 0) { if (this.core_settings.testers.indexOf(window.UserID) >= 0) {
return true; return true;
} }
@@ -254,6 +280,9 @@ export class JokerClass {
return this.check(this.core_settings.chance); return this.check(this.core_settings.chance);
} }
/**
* Стандартный запуск 1 апреля
*/
async regularStart() { async regularStart() {
if (!this.regularCheck()) { if (!this.regularCheck()) {
return; return;
@@ -263,23 +292,15 @@ export class JokerClass {
this.ui.show(); this.ui.show();
this.startAll(); this.startAll();
} }
/**
* Общие настройки системы
*/
core_settings = new CoreSettings();
/**
* Перечисление всех шуток
*/
jokes: JokeMap = {};
} }
type JokeHandler = () => void;
/** /**
* Объект хранения шуток * Объект хранения шуток
*/ */
interface JokeMap { interface JokeMap {
[key: string]: Joke; [key: string]: Joke<JokeSettings>;
} }
/** /**
@@ -289,9 +310,15 @@ interface SettingsMap {
[key: string]: JokeSettings; [key: string]: JokeSettings;
} }
/**
* Общие правила системы
*/
class CoreSettings implements JokeSettings { class CoreSettings implements JokeSettings {
chance = 100; chance = 100;
enabled = true; enabled = true;
/**
* Список пользователей-тестировщиков
*/
testers: number[] = []; testers: number[] = [];
} }
+35
View File
@@ -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;
}
+19 -14
View File
@@ -1,6 +1,5 @@
import { Joke, JokeSettings, StopableJoke } from "../joke"; import {Joke, JokeSettings} from '../joke';
import $ from "jquery"; import $ from 'jquery';
import { JokerClass } from "../joker";
/** /**
* **Карнавал** * **Карнавал**
@@ -9,15 +8,18 @@ import { JokerClass } from "../joker";
* *
* @author Kozhilya * @author Kozhilya
*/ */
export class CarnivalJoke extends Joke { export class CarnivalJoke extends Joke<CarnivalJokeSettings> {
id = "carnival"; id = 'carnival';
title = "Карнавал"; title = 'Карнавал';
description = "Аватарки приобретают интересные цвета"; description = 'Аватарки приобретают интересные цвета';
settings = new CarnivalJokeSettings(); _settings = new CarnivalJokeSettings();
/**
* Запуск шутки
*/
start(): void { start(): void {
$(this.settings.selector).each((_, elem) => { $(this.settings.selector).each((_, elem) => {
const filters: string[] = []; const filters: string[] = [];
@@ -29,19 +31,22 @@ export class CarnivalJoke extends Joke {
filters.push( filters.push(
entry[0].replace( entry[0].replace(
"{0}", '{0}',
Math.round(Math.random() * entry[1]).toString() Math.round(Math.random() * entry[1]).toString()
) )
); );
} }
if (filters.length > 0) { if (filters.length > 0) {
$(elem).css({ filter: filters.join(" ") }); $(elem).css({filter: filters.join(' ')});
} }
}); });
} }
} }
/**
* Класс настроек для шутки "Карнавал"
*/
export class CarnivalJokeSettings implements JokeSettings { export class CarnivalJokeSettings implements JokeSettings {
enabled: boolean = true; enabled: boolean = true;
@@ -61,10 +66,10 @@ export class CarnivalJokeSettings implements JokeSettings {
* Список фильтров и диапозона случайных параметров * Список фильтров и диапозона случайных параметров
*/ */
variants: CarnivalJokeFilters = { variants: CarnivalJokeFilters = {
"sepia({0}%)": 100, 'sepia({0}%)': 100,
"saturate({0}%)": 100, 'saturate({0}%)': 100,
"hue-rotate({0}deg)": 360, 'hue-rotate({0}deg)': 360,
"invert({0}%)": 100, 'invert({0}%)': 100,
}; };
} }
+17 -11
View File
@@ -1,5 +1,5 @@
import { Joke, JokeSettings, StopableJoke } from "../joke"; import {Joke, JokeSettings} from '../joke';
import $ from "jquery"; import $ from 'jquery';
/** /**
* **Мама, Я сОшЛа С уМа!** * **Мама, Я сОшЛа С уМа!**
@@ -8,22 +8,25 @@ import $ from "jquery";
* *
* @author Kozhilya * @author Kozhilya
*/ */
export class CrazyLettersJoke extends Joke { export class CrazyLettersJoke extends Joke<CrazyLettersJokeSettings> {
id = "crazy_letters"; id = 'crazy_letters';
title = "Мама, Я сОшЛа С уМа!"; title = 'Мама, Я сОшЛа С уМа!';
description = "иМеНа ЗаБоРчИкОм"; description = 'иМеНа ЗаБоРчИкОм';
settings = new CrazyLettersJokeSettings(); _settings = new CrazyLettersJokeSettings();
/**
* Запуск шутки
*/
start(): void { start(): void {
$(this.settings.selector).each((_, elem) => { $(this.settings.selector).each((_, elem) => {
let text = $(this).text(); const text = $(elem).text();
let result = ""; let result = '';
for (let i = 0; i < text.length; i++) { for (let i = 0; i < text.length; i++) {
result += text[i][i % 2 ? "toLowerCase" : "toUpperCase"](); result += text[i][i % 2 ? 'toLowerCase' : 'toUpperCase']();
} }
$(this).text(result); $(this).text(result);
@@ -31,6 +34,9 @@ export class CrazyLettersJoke extends Joke {
} }
} }
/**
* Класс настроек для шутки "Мама, Я сОшЛа С уМа!"
*/
export class CrazyLettersJokeSettings implements JokeSettings { export class CrazyLettersJokeSettings implements JokeSettings {
enabled: boolean = true; enabled: boolean = true;
@@ -39,5 +45,5 @@ export class CrazyLettersJokeSettings implements JokeSettings {
/** /**
* Селектор всех элементов, которые будут затронуты * Селектор всех элементов, которые будут затронуты
*/ */
selector: string = ".pa-author a"; selector: string = '.pa-author a';
} }
+100 -68
View File
@@ -1,8 +1,8 @@
import { Joke, JokeSettings, StopableJoke } from "../joke";
import $ from "jquery"; import {JokeSettings, StoppableJoke} from '../joke';
import { Interface } from "readline";
// import * as cursor_effects from "cursor-effects"; // eslint-disable-next-line @typescript-eslint/no-var-requires
const cursor_effects = require("cursor-effects"); import * as CursorEffectsLib from 'cursor-effects';
/** /**
* **Красивый курсор** * **Красивый курсор**
@@ -12,16 +12,17 @@ const cursor_effects = require("cursor-effects");
* @author Tim Holman * @author Tim Holman
* @link https://tholman.com/cursor-effects/ * @link https://tholman.com/cursor-effects/
*/ */
export class CursorEffectsJoke extends StopableJoke { export class CursorEffectsJoke extends
id = "cursor_effects"; StoppableJoke<CursorEffectsJokeSettings> {
id = 'cursor_effects';
title = "Красивый курсор"; title = 'Красивый курсор';
description = "Эффекты, следующие за курсором"; description = 'Эффекты, следующие за курсором';
settings = new CursorEffectsJokeSettings(); _settings = new CursorEffectsJokeSettings();
active_cursor_effect: any; active_cursor_effect: CursorEffectsLib.CursorEffectResult;
/** /**
* Разблокировать все варианты курсоров * Разблокировать все варианты курсоров
@@ -41,11 +42,21 @@ export class CursorEffectsJoke extends StopableJoke {
} }
} }
/**
* Получение веса отдельного эффекта
* @param {CursorEffectsEnum} effect Тип эффекта
* @return {number} Значение веса
* @private
*/
private weight(effect: CursorEffectsEnum): number { private weight(effect: CursorEffectsEnum): number {
const s = this.settings.effects[effect]; const s = this.settings.effects[effect];
return s.enabled ? s.weight : 0; return s.enabled ? s.weight : 0;
} }
/**
* Получение случайного эффекта
* @return {CursorEffectsEnum}
*/
getRandomEffect(): CursorEffectsEnum { getRandomEffect(): CursorEffectsEnum {
const elements = Object.values(CursorEffectsEnum).map( const elements = Object.values(CursorEffectsEnum).map(
(e) => <CursorEffectsEnum>e (e) => <CursorEffectsEnum>e
@@ -64,9 +75,12 @@ export class CursorEffectsJoke extends StopableJoke {
} }
} }
throw new Error("Тебя быть тут не должно..."); throw new Error('Тебя быть тут не должно...');
} }
/**
* Запуск шутки
*/
start(): void { start(): void {
const effect = this.getRandomEffect(); const effect = this.getRandomEffect();
const settings = this.settings.effects[effect]; const settings = this.settings.effects[effect];
@@ -74,65 +88,77 @@ export class CursorEffectsJoke extends StopableJoke {
this.active_cursor_effect = resolveCursorEffectsEnum(effect, settings); this.active_cursor_effect = resolveCursorEffectsEnum(effect, settings);
} }
/**
* Остановка шутки
*/
stop(): void { stop(): void {
this.active_cursor_effect.destroy(); this.active_cursor_effect.destroy();
} }
} }
enum CursorEffectsEnum { enum CursorEffectsEnum {
Rainbow = "rainbow", Rainbow = 'rainbow',
FairyDust = "fairy_dust", FairyDust = 'fairy_dust',
SpringyEmoji = "springy_emoji", SpringyEmoji = 'springy_emoji',
Emoji = "emoji", Emoji = 'emoji',
Ghost = "ghost", Ghost = 'ghost',
Trailing = "trailing", Trailing = 'trailing',
TextFlag = "text_flag", TextFlag = 'text_flag',
FollowingDot = "following_dot", FollowingDot = 'following_dot',
Bubbles = "bubbles", Bubbles = 'bubbles',
Snowflake = "snowflake", Snowflake = 'snowflake',
Clock = "clock", Clock = 'clock',
} }
/**
* Получение эффекта
* @param {CursorEffectsEnum | string} effect Тип эффекта
* @param {CursorEffectsSetting} options Настройки эффекта
* @return {CursorEffectsLib.CursorEffectResult}
*/
function resolveCursorEffectsEnum( function resolveCursorEffectsEnum(
effect: CursorEffectsEnum | string, effect: CursorEffectsEnum | string,
options: CursorEffectsSetting options: CursorEffectsSetting
): any { ): CursorEffectsLib.CursorEffectResult {
switch (effect) { switch (effect) {
case CursorEffectsEnum.Rainbow: case CursorEffectsEnum.Rainbow:
return cursor_effects.rainbowCursor(options); return CursorEffectsLib.rainbowCursor(options);
case CursorEffectsEnum.FairyDust: case CursorEffectsEnum.FairyDust:
return cursor_effects.fairyDustCursor(options); return CursorEffectsLib.fairyDustCursor(options);
case CursorEffectsEnum.SpringyEmoji: case CursorEffectsEnum.SpringyEmoji:
return cursor_effects.springyEmojiCursor(options); return CursorEffectsLib.springyEmojiCursor(options);
case CursorEffectsEnum.Emoji: case CursorEffectsEnum.Emoji:
return cursor_effects.emojiCursor(options); return CursorEffectsLib.emojiCursor(options);
case CursorEffectsEnum.Ghost: case CursorEffectsEnum.Ghost:
return cursor_effects.ghostCursor(options); return CursorEffectsLib.ghostCursor(options);
case CursorEffectsEnum.Trailing: case CursorEffectsEnum.Trailing:
return cursor_effects.trailingCursor(options); return CursorEffectsLib.trailingCursor(options);
case CursorEffectsEnum.TextFlag: case CursorEffectsEnum.TextFlag:
return cursor_effects.textFlag(options); return CursorEffectsLib.textFlag(options);
case CursorEffectsEnum.FollowingDot: case CursorEffectsEnum.FollowingDot:
return cursor_effects.followingDotCursor(options); return CursorEffectsLib.followingDotCursor(options);
case CursorEffectsEnum.Bubbles: case CursorEffectsEnum.Bubbles:
return cursor_effects.bubbleCursor(options); return CursorEffectsLib.bubbleCursor(options);
case CursorEffectsEnum.Snowflake: case CursorEffectsEnum.Snowflake:
return cursor_effects.snowflakeCursor(options); return CursorEffectsLib.snowflakeCursor(options);
case CursorEffectsEnum.Clock: case CursorEffectsEnum.Clock:
return cursor_effects.clockCursor(options); return CursorEffectsLib.clockCursor(options);
} }
} }
/**
* Класс настроек для шутки "Красивый курсор"
*/
export class CursorEffectsJokeSettings implements JokeSettings { export class CursorEffectsJokeSettings implements JokeSettings {
enabled: boolean = true; enabled: boolean = true;
@@ -143,7 +169,7 @@ export class CursorEffectsJokeSettings implements JokeSettings {
* Радужный след за курсором * Радужный след за курсором
*/ */
[CursorEffectsEnum.Rainbow]: [CursorEffectsEnum.Rainbow]:
new (class extends DefaultCursorEffectsSetting { new (class extends CursorEffectsSetting {
/** /**
* Длина полосы * Длина полосы
*/ */
@@ -158,13 +184,13 @@ export class CursorEffectsJokeSettings implements JokeSettings {
* Цвета полос (сверху вниз) * Цвета полос (сверху вниз)
*/ */
colors: string[] = [ colors: string[] = [
"#9400D3", '#9400D3',
"#4B0082", '#4B0082',
"#0000FF", '#0000FF',
"#00FF00", '#00FF00',
"#FFFF00", '#FFFF00',
"#FF7F00", '#FF7F00',
"#FF0000", '#FF0000',
]; ];
})(), })(),
@@ -172,44 +198,44 @@ export class CursorEffectsJokeSettings implements JokeSettings {
* Цепочка из эмодзи "на резинке" под курсором * Цепочка из эмодзи "на резинке" под курсором
*/ */
[CursorEffectsEnum.SpringyEmoji]: [CursorEffectsEnum.SpringyEmoji]:
new (class extends DefaultCursorEffectsSetting { new (class extends CursorEffectsSetting {
/** /**
* Висящий эмодзи * Висящий эмодзи
*/ */
emoji: string = "🤪"; emoji: string = '🤪';
})(), })(),
/** /**
* Падающие от курсора эмодзи * Падающие от курсора эмодзи
*/ */
[CursorEffectsEnum.Emoji]: new (class extends DefaultCursorEffectsSetting { [CursorEffectsEnum.Emoji]: new (class extends CursorEffectsSetting {
/** /**
* Падающие эмодзи * Падающие эмодзи
*/ */
emoji: string[] = ["🔥", "🐬", "🦆"]; emoji: string[] = ['🔥', '🐬', '🦆'];
})(), })(),
/** /**
* Падающие от курсора пылинки * Падающие от курсора пылинки
*/ */
[CursorEffectsEnum.FairyDust]: [CursorEffectsEnum.FairyDust]:
new (class extends DefaultCursorEffectsSetting { new (class extends CursorEffectsSetting {
/** /**
* Цвет пылинок * Цвет пылинок
*/ */
colors: string[] = ["#ff0000", "#00ff00", "#0000ff"]; colors: string[] = ['#ff0000', '#00ff00', '#0000ff'];
})(), })(),
/** /**
* След "зависших" курсоров * След "зависших" курсоров
*/ */
[CursorEffectsEnum.Ghost]: new DefaultCursorEffectsSetting(), [CursorEffectsEnum.Ghost]: new CursorEffectsSetting(),
/** /**
* След курсоров "на резинке" * След курсоров "на резинке"
*/ */
[CursorEffectsEnum.Trailing]: [CursorEffectsEnum.Trailing]:
new (class extends DefaultCursorEffectsSetting { new (class extends CursorEffectsSetting {
/** /**
* Количество элементов * Количество элементов
*/ */
@@ -231,21 +257,21 @@ export class CursorEffectsJokeSettings implements JokeSettings {
* Текст, развивающийся флагом от курсора * Текст, развивающийся флагом от курсора
*/ */
[CursorEffectsEnum.TextFlag]: [CursorEffectsEnum.TextFlag]:
new (class extends DefaultCursorEffectsSetting { new (class extends CursorEffectsSetting {
/** /**
* Отображаемый текст * Отображаемый текст
*/ */
text: string = "С днём смеха!"; text: string = 'С днём смеха!';
/** /**
* Цвет текста * Цвет текста
*/ */
color: string = "#000000"; color: string = '#000000';
/** /**
* Шрифт текста * Шрифт текста
*/ */
font: string = "monospace"; font: string = 'monospace';
/** /**
* Размер текста * Размер текста
@@ -267,17 +293,17 @@ export class CursorEffectsJokeSettings implements JokeSettings {
* Точка, преследующая курсор * Точка, преследующая курсор
*/ */
[CursorEffectsEnum.FollowingDot]: [CursorEffectsEnum.FollowingDot]:
new (class extends DefaultCursorEffectsSetting { new (class extends CursorEffectsSetting {
/** /**
* Цвет догоняющей точки * Цвет догоняющей точки
*/ */
color: string = "#323232a6"; color: string = '#323232a6';
})(), })(),
/** /**
* Пузырьки от курсора * Пузырьки от курсора
*/ */
[CursorEffectsEnum.Bubbles]: new DefaultCursorEffectsSetting(), [CursorEffectsEnum.Bubbles]: new CursorEffectsSetting(),
/** /**
* Снежинки, летящие от курсора * Снежинки, летящие от курсора
@@ -285,47 +311,53 @@ export class CursorEffectsJokeSettings implements JokeSettings {
* Отключено, потому что сейчас не зима :D * Отключено, потому что сейчас не зима :D
*/ */
[CursorEffectsEnum.Snowflake]: [CursorEffectsEnum.Snowflake]:
new (class extends DefaultCursorEffectsSetting { new (class extends CursorEffectsSetting {
weight: number = 0; weight: number = 0;
})(), })(),
/** /**
* Циферблат вокруг курсора * Циферблат вокруг курсора
*/ */
[CursorEffectsEnum.Clock]: new (class extends DefaultCursorEffectsSetting { [CursorEffectsEnum.Clock]: new (class extends CursorEffectsSetting {
/** /**
* Цвет даты * Цвет даты
*/ */
dateColor: string = "blue"; dateColor: string = 'blue';
/** /**
* Цвет циферблата * Цвет циферблата
*/ */
faceColor: string = "black"; faceColor: string = 'black';
/** /**
* Цвет секундной стрелки * Цвет секундной стрелки
*/ */
secondsColor: string = "red"; secondsColor: string = 'red';
/** /**
* Цвет минутной стрелки * Цвет минутной стрелки
*/ */
minutesColor: string = "black"; minutesColor: string = 'black';
/** /**
* Цвет часовой стрелки * Цвет часовой стрелки
*/ */
hoursColor: string = "black"; hoursColor: string = 'black';
})(), })(),
}; };
} }
interface CursorEffectsSetting { /**
[key: string]: any; * Восстановление класса CursorEffectsLib.DefaultOptions
*/
class DefaultOptions {
readonly element?: HTMLElement;
} }
class DefaultCursorEffectsSetting implements CursorEffectsSetting { /**
* Класс настроек для курсора
*/
class CursorEffectsSetting extends DefaultOptions {
/** /**
* Флаг возможности срабатывания * Флаг возможности срабатывания
*/ */
+362
View File
@@ -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;
}
+21 -10
View File
@@ -1,6 +1,5 @@
import { Joke, JokeSettings, StopableJoke } from "../joke"; import {Joke, JokeSettings} from '../joke';
import { getTextNodes } from "../common"; import {getTextNodes} from '../common';
import $ from "jquery";
/** /**
* **Ад гуманитария** * **Ад гуманитария**
@@ -9,21 +8,30 @@ import $ from "jquery";
* *
* @author Kozhilya * @author Kozhilya
*/ */
export class HumanitarianHellJoke extends Joke { export class HumanitarianHellJoke extends Joke<HumanitarianHellJokeSettings> {
id = "humanitarian_hell"; id = 'humanitarian_hell';
title = "Ад гуманитария"; title = 'Ад гуманитария';
description = 'Все "тся" заменяются на "ться", а все "ться" на "тся"'; description = 'Все "тся" заменяются на "ться", а все "ться" на "тся"';
settings = new HumanitarianHellJokeSettings(); _settings = new HumanitarianHellJokeSettings();
/**
* Обработка текста
*
* @param {string} text Входная строка
* @return {string} Итоговая строка
*/
processText(text: string): string { processText(text: string): string {
return text.replace(/(ть?ся)/g, function (fnd) { return text.replace(/(ть?ся)/g, function(fnd) {
return fnd.indexOf("ь") > 0 ? "тся" : "ться"; return fnd.indexOf('ь') > 0 ? 'тся' : 'ться';
}); });
} }
/**
* Запуск шутки
*/
start(): void { start(): void {
const textNodes = getTextNodes(this.settings.selector); const textNodes = getTextNodes(this.settings.selector);
@@ -33,6 +41,9 @@ export class HumanitarianHellJoke extends Joke {
} }
} }
/**
* Класс настроек для шутки "Ад гуманитария"
*/
export class HumanitarianHellJokeSettings implements JokeSettings { export class HumanitarianHellJokeSettings implements JokeSettings {
enabled: boolean = true; enabled: boolean = true;
@@ -41,5 +52,5 @@ export class HumanitarianHellJokeSettings implements JokeSettings {
/** /**
* Селектор всех элементов, которые будут затронуты * Селектор всех элементов, которые будут затронуты
*/ */
selector: string = ".post-content"; selector: HTMLElement | JQuery<HTMLElement> | string = '.post-content';
} }
+40
View File
@@ -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;
}
+106
View File
@@ -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': 'з',
};
}
+30 -19
View File
@@ -1,7 +1,5 @@
import { Joke, JokeSettings, StopableJoke } from "../joke"; import {Joke, JokeSettings} from '../joke';
import { getTextNodes } from "../common"; import {getTextNodes} from '../common';
import $ from "jquery";
import { JokerClass } from "../joker";
/** /**
* **Буквенный хаос** * **Буквенный хаос**
@@ -10,27 +8,30 @@ import { JokerClass } from "../joker";
* *
* @author Kozhilya * @author Kozhilya
*/ */
export class LetterChaosJoke extends Joke { export class LetterChaosJoke extends Joke<LetterChaosJokeSettings> {
id = "letter_chaos"; id = 'letter_chaos';
title = "Буквенный хаос"; title = 'Буквенный хаос';
description = "Все буквы в словах, кроме первой и последней, перемешены"; description = 'Все буквы в словах, кроме первой и последней, перемешены';
settings = new LetterChaosJokeSettings(); _settings = new LetterChaosJokeSettings();
punctuationSymbols: string[] | null = null; punctuationSymbols: string[] | null = null;
/**
* Перемешивание букв в словах
* @param {string} word
* @return {string}
* @private
*/
private shuffleLetters(word: string): string { private shuffleLetters(word: string): string {
// Check if the word has two or more letters
if (word.length < 3 || !this.check(this.settings.shuffle_chance)) { if (word.length < 3 || !this.check(this.settings.shuffle_chance)) {
return word; return word;
} }
// Split the word into an array of characters const characters = word.split('');
const characters = word.split("");
// Shuffle the characters between the first and last letters
const middleCharacters = characters.slice(1, -1); const middleCharacters = characters.slice(1, -1);
for (let i = middleCharacters.length - 1; i > 0; i--) { for (let i = middleCharacters.length - 1; i > 0; i--) {
const j = Math.floor(Math.random() * (i + 1)); const j = Math.floor(Math.random() * (i + 1));
@@ -40,28 +41,38 @@ export class LetterChaosJoke extends Joke {
]; ];
} }
// Join the shuffled characters back into a word
return [ return [
characters[0], characters[0],
...middleCharacters, ...middleCharacters,
characters[characters.length - 1], characters[characters.length - 1],
].join(""); ].join('');
} }
private processNode(node: any) { /**
* Обработка текстовой ноды
* @param {Text} node
* @private
*/
private processNode(node: Text) {
node.textContent = node.textContent?.replace( node.textContent = node.textContent?.replace(
/([А-Яа-яA-Za-z]+)/gm, /([А-Яа-яA-Za-z]+)/gm,
this.shuffleLetters (_: string, word: string) => this.shuffleLetters(word)
); );
} }
/**
* Запуск шутки
*/
start(): void { start(): void {
const textNodes = getTextNodes(this.settings.selector); const textNodes = getTextNodes(this.settings.selector);
textNodes.each((_, node) => this.processNode(node)); textNodes.each((_, node) => this.processNode(node as Text));
} }
} }
/**
* Класс настроек для шутки "Буквенный хаос"
*/
export class LetterChaosJokeSettings implements JokeSettings { export class LetterChaosJokeSettings implements JokeSettings {
enabled: boolean = true; enabled: boolean = true;
@@ -70,7 +81,7 @@ export class LetterChaosJokeSettings implements JokeSettings {
/** /**
* Селектор всех элементов, которые будут затронуты * Селектор всех элементов, которые будут затронуты
*/ */
selector: string = ".post-content"; selector: string = '.post-content';
/** /**
* Вероятность того, что буквы будут перемешаны * Вероятность того, что буквы будут перемешаны
+21 -15
View File
@@ -1,5 +1,5 @@
import { Joke, JokeSettings, StopableJoke } from "../joke"; import {Joke, JokeSettings} from '../joke';
import $ from "jquery"; import $ from 'jquery';
/** /**
* **Зеркало** * **Зеркало**
@@ -8,27 +8,33 @@ import $ from "jquery";
* *
* @author Kozhilya * @author Kozhilya
*/ */
export class MirrorJoke extends Joke { export class MirrorJoke extends Joke<MirrorJokeSettings> {
id = "mirror"; id = 'mirror';
title = "Зеркало"; title = 'Зеркало';
description = "Элементы форума отражаются (по крайней мере, пытаются)"; description = 'Элементы форума отражаются (по крайней мере, пытаются)';
settings = new MirrorJokeSettings(); _settings = new MirrorJokeSettings();
/**
* Запуск шутки
*/
start(): void { start(): void {
$(".post, .post h3, .post-links, .post-author, .post-content") $('.post, .post h3, .post-links, .post-author, .post-content')
.css("transform", "scaleX(-1)") .css('transform', 'scaleX(-1)')
.css("-moz-transform", "scaleX(-1)") .css('-moz-transform', 'scaleX(-1)')
.css("-moz-transform", "scaleX(-1)") .css('-moz-transform', 'scaleX(-1)')
.css("filter", "FlipH") .css('filter', 'FlipH')
.css("-ms-filter", '"FlipH"'); .css('-ms-filter', '"FlipH"');
$(".post-links, .post-links ul, .post h3 span").css("margin-left", "0"); $('.post-links, .post-links ul, .post h3 span').css('margin-left', '0');
$(".post h3 span").css("margin-right", "25em"); $('.post h3 span').css('margin-right', '25em');
} }
} }
/**
* Класс настроек для шутки "Зеркало"
*/
export class MirrorJokeSettings implements JokeSettings { export class MirrorJokeSettings implements JokeSettings {
enabled: boolean = false; enabled: boolean = false;
+37 -22
View File
@@ -1,5 +1,5 @@
import { Joke, JokeSettings, StopableJoke } from "../joke"; import {JokeSettings, StoppableJoke} from '../joke';
import $ from "jquery"; import $ from 'jquery';
/** /**
* **Форумо-трясение** * **Форумо-трясение**
@@ -8,73 +8,88 @@ import $ from "jquery";
* *
* @author Kozhilya * @author Kozhilya
*/ */
export class ShakeJoke extends StopableJoke { export class ShakeJoke extends StoppableJoke<ShakeJokeSettings> {
id = "shake"; id = 'shake';
title = "Форумо-трясение"; title = 'Форумо-трясение';
description = "Страница форума начинает трястись"; description = 'Страница форума начинает трястись';
settings = new ShakeJokeSettings(); _settings = new ShakeJokeSettings();
private interval_id: NodeJS.Timer; private intervalId: NodeJS.Timeout;
private items: JQuery; private items: JQuery;
private data_name: string = "april-fools-shake"; private dataName: string = 'april-fools-shake';
/**
* Выбор случайной амплитуды
* @param {number} a
* @return {number}
*/
rand(a: number): number { rand(a: number): number {
return a + this.settings.force * (Math.random() * 2 - 1); return a + this.settings.force * (Math.random() * 2 - 1);
} }
/**
* Запуск шутки
*/
start(): void { start(): void {
this.items = $(this.settings.selector); this.items = $(this.settings.selector);
this.items.each((_, elem) => { this.items.each((_, elem) => {
let data: ShakeJokeMargin = {}; const data: ShakeJokeMargin = {};
this.settings.directions.forEach((d) => { this.settings.directions.forEach((d) => {
data[d] = parseInt($(elem).css("margin-" + d)); data[d] = parseInt($(elem).css('margin-' + d));
}); });
$(elem).data(this.data_name, data); $(elem).data(this.dataName, data);
}); });
this.interval_id = setInterval(() => { this.intervalId = setInterval(() => {
this.items.each((_, elem) => { this.items.each((_, elem) => {
const data: ShakeJokeMargin = $(elem).data(this.data_name); const data: ShakeJokeMargin = $(elem).data(this.dataName);
for (const entry of Object.entries(data)) { for (const entry of Object.entries(data)) {
$(elem).css("margin-" + entry[0], this.rand(entry[1])); $(elem).css('margin-' + entry[0], this.rand(entry[1]));
} }
}); });
}, this.settings.interval); }, this.settings.interval);
} }
/**
* Остновка шутки
*/
stop(): void { stop(): void {
clearInterval(this.interval_id); clearInterval(this.intervalId);
this.items.each((_, elem) => { this.items.each((_, elem) => {
let data: ShakeJokeMargin = {}; const data: ShakeJokeMargin = {};
this.settings.directions.forEach((d) => { this.settings.directions.forEach((d) => {
$(elem).css("margin-" + d, data[d]); $(elem).css('margin-' + d, data[d]);
}); });
$(elem).data(this.data_name, data); $(elem).data(this.dataName, data);
}); });
} }
} }
/**
* Класс настроек для шутки "Форумо-трясение"
*/
export class ShakeJokeSettings implements JokeSettings { export class ShakeJokeSettings implements JokeSettings {
enabled: boolean = false; enabled: boolean = false;
chance: number = 1; chance: number = 1;
/** /**
* Направление тряски (тряска происходит путём изменения перечисленных направлений margin на значение ±force от стандартного значения) * Направление тряски (тряска происходит путём изменения перечисленных
* направлений margin на значение ±force от стандартного значения)
*/ */
directions: string[] = ["top", "left", "bottom", "left"]; directions: string[] = ['top', 'left', 'bottom', 'left'];
/** /**
* Сила тряски - насколько отклоняется элемент от стандартного значения * Сила тряски - насколько отклоняется элемент от стандартного значения
@@ -84,7 +99,7 @@ export class ShakeJokeSettings implements JokeSettings {
/** /**
* Селектор всех элементов, которые будут трястись * Селектор всех элементов, которые будут трястись
*/ */
selector: string = "[id^=pun]"; selector: string = '[id^=pun]';
/** /**
* Частота тряски * Частота тряски
+182 -93
View File
@@ -1,6 +1,6 @@
import { Joke, JokeSettings, StopableJoke } from "../joke"; import {Joke, JokeSettings} from '../joke';
import $ from "jquery"; import $ from 'jquery';
import { StupidTitleDefaultVariants } from "../data/stupid_title_variants"; import {StupidTitleDefaultVariants} from '../data/stupid_title_variants';
/** /**
* **Глупые статусы** * **Глупые статусы**
@@ -9,22 +9,27 @@ import { StupidTitleDefaultVariants } from "../data/stupid_title_variants";
* *
* @author Kozhilya * @author Kozhilya
*/ */
export class StupidTitleJoke extends Joke { export class StupidTitleJoke extends Joke<StupidTitleJokeSettings> {
id = "stupid_title"; id = 'stupid_title';
title = "Глупые статусы"; title = 'Глупые статусы';
description = "Каждому пользователю даётся новый глупый статус"; description = 'Каждому пользователю даётся новый глупый статус';
settings = new StupidTitleJokeSettings(); _settings = new StupidTitleJokeSettings();
private loadedGenders: any = null; private loadedGenders: { [key in string]: boolean } = null;
private variantsGenerated = false;
private seed = 0;
/**
* Загрузка полов, если они не определены
*/
async loadGenders(): Promise<void> { async loadGenders(): Promise<void> {
const names = $(".pa-author a") const names = $('.pa-author a').
.map((_, elem) => elem.innerText) map((_, elem) => elem.innerText).
.toArray() toArray().
.reduce((arr: string[], name: string) => { reduce((arr: string[], name: string) => {
if (arr.indexOf(name) < 0) { if (arr.indexOf(name) < 0) {
arr.push(name); arr.push(name);
} }
@@ -32,8 +37,8 @@ export class StupidTitleJoke extends Joke {
}, []); }, []);
const url = const url =
"/api.php?" + '/api.php?' +
$.param({ method: "users.get", fields: "username,sex", username: names }); $.param({method: 'users.get', fields: 'username,sex', username: names});
const response = await fetch(url); const response = await fetch(url);
const json = await response.json(); const json = await response.json();
@@ -43,15 +48,21 @@ export class StupidTitleJoke extends Joke {
return; return;
} }
json.response.users.forEach((element: any) => { json.response?.users.forEach(
if (element.sex === "0") { (element: { username: string, sex: string }) => {
if (element.sex === '0') {
return; return;
} }
this.loadedGenders[element.username] = element.sex === "1"; this.loadedGenders[element.username] = element.sex === '1';
}); });
} }
/**
* Разрешение пола по имени
* @param {string} name
* @return {boolean}
*/
resolveGender(name: string): boolean { resolveGender(name: string): boolean {
if (this.settings.gender_resolver !== null) { if (this.settings.gender_resolver !== null) {
return !!this.settings.gender_resolver.call(this, name); return !!this.settings.gender_resolver.call(this, name);
@@ -62,11 +73,12 @@ export class StupidTitleJoke extends Joke {
} }
this.resetSeed(); this.resetSeed();
return this.selectRandom(name, { m: 1, f: 1 }) === "m"; return this.selectRandom(name, {m: 1, f: 1}) === 'm';
} }
private variantsGenerated = false; /**
* Запуск шутки
*/
async start(): Promise<void> { async start(): Promise<void> {
if (!this.variantsGenerated) { if (!this.variantsGenerated) {
StupidTitleDefaultVariants(this.settings.variants); StupidTitleDefaultVariants(this.settings.variants);
@@ -75,10 +87,10 @@ export class StupidTitleJoke extends Joke {
await this.loadGenders(); await this.loadGenders();
const cache: any = {}; const cache: {[key in string]: string} = {};
$(".post-author").each((_, root) => { $('.post-author').each((_, root) => {
const name = $(".pa-author a", root).text(); const name = $('.pa-author a', root).text();
if (!name) { if (!name) {
return; return;
} }
@@ -88,21 +100,47 @@ export class StupidTitleJoke extends Joke {
cache[name] = this.generatePhrase(name); cache[name] = this.generatePhrase(name);
} }
$(".pa-title", root).text(cache[name]); $('.pa-title', root).text(cache[name]);
}); });
} }
private prng(name: string, seed: number, limit: number): number { /**
/* * Сгенерировать фразу
https://github.com/bryc/code/blob/master/jshash/experimental/cyrb53.js * @param {string} name Имя, кому необходимо сгенерировать фразу
* @return {string} Итоговая фраза
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.
*/ */
let h1 = 0xdeadbeef ^ seed, generatePhrase(name: string): string {
h2 = 0x41c6ce57 ^ seed; 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++) { for (let i = 0, ch; i < name.length; i++) {
ch = name.charCodeAt(i); ch = name.charCodeAt(i);
h1 = Math.imul(h1 ^ ch, 2654435761); h1 = Math.imul(h1 ^ ch, 2654435761);
@@ -120,8 +158,10 @@ export class StupidTitleJoke extends Joke {
return Math.floor(Math.pow(Math.sin(value), 2) * limit) % limit; return Math.floor(Math.pow(Math.sin(value), 2) * limit) % limit;
} }
private seed = 0; /**
* Сброс зерна случайности
* @private
*/
private resetSeed(): void { private resetSeed(): void {
const date = new Date(); const date = new Date();
const time = date.getUTCHours() * 60 + date.getUTCMinutes(); const time = date.getUTCHours() * 60 + date.getUTCMinutes();
@@ -134,14 +174,21 @@ export class StupidTitleJoke extends Joke {
this.seed = Math.floor(time / this.settings.change_frequency); this.seed = Math.floor(time / this.settings.change_frequency);
} }
/**
* Выбор случайного слова
* @param {string} name
* @param {StupidTitleWeightedVariants} variants
* @return {string}
* @private
*/
private selectRandom( private selectRandom(
name: string, name: string,
variants: StupidTitleWeightedVariants variants: StupidTitleWeightedVariants,
): string { ): string {
const elements = Object.keys(variants); const elements = Object.keys(variants);
const totalWeight = elements.reduce( const totalWeight = elements.reduce(
(sum, element) => sum + variants[element], (sum, element) => sum + variants[element],
0 0,
); );
const randomWeight = this.prng(name, this.seed++, totalWeight); const randomWeight = this.prng(name, this.seed++, totalWeight);
@@ -153,12 +200,19 @@ export class StupidTitleJoke extends Joke {
} }
} }
throw new Error("Тебя быть тут не должно..."); throw new Error('Тебя быть тут не должно...');
} }
/**
* Выбор случайной фразы
* @param {string} type
* @param {boolean} gender
* @return {StupidTitleWeightedVariants}
* @private
*/
private buildVariants( private buildVariants(
type: string, type: string,
gender: boolean gender: boolean,
): StupidTitleWeightedVariants { ): StupidTitleWeightedVariants {
const result: StupidTitleWeightedVariants = {}; const result: StupidTitleWeightedVariants = {};
const variants: StupidTitleVariant = this.settings.variants[type]; const variants: StupidTitleVariant = this.settings.variants[type];
@@ -181,24 +235,27 @@ export class StupidTitleJoke extends Joke {
return result; return result;
} }
/**
* Сделать первую буква слова заглавной
* @param {string} str Слово
* @return {string} Исправленное слово
* @private
*/
private capitalizeFirst(str: string): string { private capitalizeFirst(str: string): string {
return str.charAt(0).toUpperCase() + str.slice(1); return str.charAt(0).toUpperCase() + str.slice(1);
} }
generatePhrase(name: string): string {
const gender = this.resolveGender(name);
const prhase = this.selectRandom(name, this.settings.schemas);
return this.capitalizeFirst(
prhase.replace(/\{(.+?)\}/g, (_, type: string) => {
const variants = this.buildVariants(type, gender);
return this.selectRandom(name, variants);
})
);
}
} }
/**
* Определение пола по имени.
* @param {string} name Имя персонажа
* @return {boolean | null} true, если мужской род; false, если женский род
*/
type GenderResolverFunction = (name: string) => (boolean | null);
/**
* Класс настроек для шутки "Глупые статусы"
*/
export class StupidTitleJokeSettings implements JokeSettings { export class StupidTitleJokeSettings implements JokeSettings {
enabled: boolean = true; enabled: boolean = true;
chance: number = 100; chance: number = 100;
@@ -207,12 +264,13 @@ export class StupidTitleJokeSettings implements JokeSettings {
* Определение пола по имени. * Определение пола по имени.
* По умолчанию род устанавливается случайным образом * По умолчанию род устанавливается случайным образом
* *
* Если установлено значение `null`, система попытается загрузить пол, установленный в настройках аккаунта. Если этот пол определить невозможно, то он будет выбран случайно. Эта настройка позволяет изменить это поведение, если ваш форум обладает ролевой системой, в которой пол определяется иным образом. * Если установлено значение `null`, система попытается загрузить пол,
* * установленный в настройках аккаунта. Если этот пол определить невозможно,
* @param name Имя персонажа * то он будет выбран случайно. Эта настройка позволяет изменить это
* @returns true, если мужской род; false, если женский род * поведение, если ваш форум обладает ролевой системой, в которой пол
* определяется иным образом.
*/ */
gender_resolver: Function | null = null; gender_resolver: GenderResolverFunction | null = null;
/** /**
* Разрешить возможно оскорбительные варианты * Разрешить возможно оскорбительные варианты
@@ -221,7 +279,8 @@ export class StupidTitleJokeSettings implements JokeSettings {
/** /**
* Частота смены статусов (в минутах) * Частота смены статусов (в минутах)
* Установите значение 0, что бы обновление происходило при каждом обновлении страницы * Установите значение 0, что бы обновление происходило при
* каждом обновлении страницы
*/ */
change_frequency: number = 5; change_frequency: number = 5;
@@ -236,14 +295,14 @@ export class StupidTitleJokeSettings implements JokeSettings {
* {p} - знак препинания * {p} - знак препинания
*/ */
schemas: StupidTitleWeightedVariants = { schemas: StupidTitleWeightedVariants = {
"{adj} {noun}{p}": 10, '{adj} {noun}{p}': 10,
"{noun} {adj}{p}": 3, '{noun} {adj}{p}': 3,
"{adj}, {adj} {noun}{p}": 10, '{adj}, {adj} {noun}{p}': 10,
"{adj} и очень {adj} {noun}{p}": 10, '{adj} и очень {adj} {noun}{p}': 10,
"{adj} {noun} {verb}{p}": 10, '{adj} {noun} {verb}{p}': 10,
"{adj} {noun} {adv} {verb}{p}": 5, '{adj} {noun} {adv} {verb}{p}': 5,
"{adj} {noun} {verb} {place}{p}": 5, '{adj} {noun} {verb} {place}{p}': 5,
"{adj} {noun} {adv} {verb} {place}{p}": 5, '{adj} {noun} {adv} {verb} {place}{p}': 5,
}; };
/** /**
@@ -257,13 +316,13 @@ export class StupidTitleJokeSettings implements JokeSettings {
place: {}, place: {},
adv: {}, adv: {},
p: { p: {
"": new StupidTitleSetting({ weight: 10 }), '': new StupidTitleSetting({weight: 15}),
".": new StupidTitleSetting({ weight: 10 }), '.': new StupidTitleSetting({weight: 10}),
"...": new StupidTitleSetting({ weight: 5 }), '...': new StupidTitleSetting({weight: 5}),
"!": new StupidTitleSetting({ weight: 5 }), '!': new StupidTitleSetting({weight: 5}),
"!!!": new StupidTitleSetting({ weight: 2 }), '!!!': new StupidTitleSetting({weight: 2}),
"?": new StupidTitleSetting({ weight: 2 }), '?': new StupidTitleSetting({weight: 2}),
"?!": new StupidTitleSetting({ weight: 1 }), '?!': new StupidTitleSetting({weight: 1}),
}, },
}; };
} }
@@ -280,6 +339,16 @@ interface StupidTitleWeightedVariants {
[key: string]: number; [key: string]: number;
} }
type StupidTitleSettingInitializer = {
weight?: number;
enabled?: boolean;
rude?: boolean;
gender?: boolean | null;
}
/**
* Класс настройки
*/
class StupidTitleSetting { class StupidTitleSetting {
weight: number = 10; weight: number = 10;
@@ -289,42 +358,62 @@ class StupidTitleSetting {
gender: boolean | null = null; gender: boolean | null = null;
constructor(options: any = new Object()) { /**
for (const iterator of Object.entries(options)) { * Создание класса
if (iterator[0] in this) { * @param {StupidTitleSettingInitializer} options
(<any>this)[iterator[0]] = iterator[1]; */
} constructor(options: StupidTitleSettingInitializer) {
} this.weight ??= options.weight;
this.enabled ??= options.enabled;
this.rude ??= options.rude;
this.gender ??= options.gender;
} }
} }
export function BuildStupdiTitleVariant( /**
* Метод создания списка слов
* @param {boolean | null} gender
* @param {string} input
* @return {StupidTitleVariant}
* @constructor
*/
export function BuildStupidTitleVariant(
gender: boolean | null, gender: boolean | null,
input: string input: string,
): StupidTitleVariant { ): StupidTitleVariant {
const result: StupidTitleVariant = {}; const result: StupidTitleVariant = {};
const words = input.split(",").map((word) => word.trim()); const words = input.split(',').map((word) => word.trim());
words.forEach((word) => { words.forEach((word) => {
const clean_word = word.replace(/[^А-ЯЁа-яёA-Za-z\- ]/g, ""); const cleanWord = word.replace(/[^А-ЯЁа-яёA-Za-z\- ]/g, '');
if (clean_word === "") { if (cleanWord === '') {
return; return;
} }
result[clean_word] = new StupidTitleSetting({ result[cleanWord] = new StupidTitleSetting({
gender, gender,
rude: word.indexOf("$") >= 0, rude: word.indexOf('$') >= 0,
enabled: word.indexOf("!") < 0, enabled: word.indexOf('!') < 0,
weight: weight:
10 * 10 *
(word.indexOf("$") >= 0 ? 0.5 : 1) * (word.indexOf('$') >= 0 ? 0.5 : 1) *
(word.indexOf("%") >= 0 ? 0.5 : 1) * (word.indexOf('%') >= 0 ? 0.5 : 1) *
(word.indexOf("^") >= 0 ? 2 : 1), (word.indexOf('^') >= 0 ? 2 : 1),
}); });
}); });
return result; return result;
} }
(window as any).BuildStupdiTitleVariant = BuildStupdiTitleVariant; declare global {
interface Window {
BuildStupidTitleVariant(
gender: boolean | null,
input: string,
): StupidTitleVariant;
}
}
window.BuildStupidTitleVariant = BuildStupidTitleVariant;
+150
View File
@@ -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);
+24 -14
View File
@@ -1,19 +1,26 @@
import { JokerClass } from "./joker"; import {JokerClass} from './joker';
import { Joke } from "./joke"; import {Joke, JokeSettings} from './joke';
import { ShakeJoke } from "./jokes/shake"; import {ShakeJoke} from './jokes/shake';
import { HumanitarianHellJoke } from "./jokes/humanitarian_hell"; import {HumanitarianHellJoke} from './jokes/humanitarian_hell';
import { MirrorJoke } from "./jokes/mirror"; import {MirrorJoke} from './jokes/mirror';
import { CarnivalJoke } from "./jokes/carnival"; import {CarnivalJoke} from './jokes/carnival';
import { CrazyLettersJoke } from "./jokes/crazy_letters"; import {CrazyLettersJoke} from './jokes/crazy_letters';
import { LetterChaosJoke } from "./jokes/letter_chaos"; import {LetterChaosJoke} from './jokes/letter_chaos';
import { CursorEffectsJoke } from "./jokes/cursor"; import {CursorEffectsJoke} from './jokes/cursor';
import { StupidTitleJoke } from "./jokes/stupid_title"; 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): JokerClass => { declare global {
interface Window { AprilJokes: JokerClass; }
}
window.AprilJokes = ((jokes: JokeList): JokerClass => {
const cl = new JokerClass(); const cl = new JokerClass();
jokes.forEach((joke) => { jokes.forEach((CustomJoke) => {
cl.add(new joke()); cl.add(new CustomJoke());
}); });
$(() => cl.regularStart()); $(() => cl.regularStart());
@@ -28,6 +35,9 @@ import { StupidTitleJoke } from "./jokes/stupid_title";
LetterChaosJoke, LetterChaosJoke,
CursorEffectsJoke, CursorEffectsJoke,
StupidTitleJoke, StupidTitleJoke,
InvertedScrollJoke,
LatinizationJoke,
FreshGrassJoke,
]); ]);
interface JokeClassList extends Array<new () => Joke> {} interface JokeList extends Array<new () => Joke<JokeSettings>> {}
+108
View File
@@ -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;
}
+28
View File
@@ -0,0 +1,28 @@
/**
* Объект утилит ForumDUtils
*/
export class ForumDUtils {
/**
* Проверяет, разрешён ли запуск скрипта
*/
check(): Promise<boolean>;
/**
* Ошибка, сообщающая о невозможности запуска скрипта
*/
error(): Error;
/**
* Набор настроек скриптов
*/
scripts: {
[key in string]: ForumDUtilsScript;
};
}
/**
* Класс настроек скриптов
*/
export abstract class ForumDUtilsScript {
}
+1 -1
View File
@@ -1,4 +1,4 @@
declare module "*.html" { declare module '*.html' {
const content: string; const content: string;
export default content; export default content;
} }
+335
View File
@@ -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;
}
}
+48 -24
View File
@@ -1,23 +1,34 @@
/// <reference path="html.d.ts"/> import './types/html.d';
import { JokerClass } from "./joker"; import './types/mybb.d';
import $ from "jquery";
import button_html from "./html/button.html";
import styles_html from "./html/styles.html";
import window_html from "./html/window.html";
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 { export class JokerUI {
joker: JokerClass; joker: JokerClass;
/**
* Создание класса интерфейса настроек
*
* @param {JokerClass} joker
*/
constructor(joker: JokerClass) { constructor(joker: JokerClass) {
this.joker = joker; this.joker = joker;
$('link[rel="stylesheet"]').eq(0).before(styles_html); $('link[rel="stylesheet"]').eq(0).before(stylesHTML);
const button = this.button; const button = this.button;
button.hide(); button.hide();
(<any>button).mybbModal({
title: "Настройки первоапрельских шуток", button.mybbModal({
title: 'Настройки первоапрельских шуток',
content: () => this.render(), content: () => this.render(),
closer: true, closer: true,
noscroll: true, noscroll: true,
@@ -25,40 +36,53 @@ export class JokerUI {
}); });
} }
get button() { /**
let button = $(".april-fools__button"); * Основная кнопка, открывающая настройки
* @return {JQuery<HTMLButtonElement>}
*/
get button(): JQuery<HTMLButtonElement> {
let button = $('.april-fools__button');
if (button.length === 0) { if (button.length === 0) {
button = $(button_html).appendTo("body"); button = $(buttonHTML).appendTo('body');
} }
return button; return button as JQuery<HTMLButtonElement>;
} }
/**
* Показать кнопку настроек
*/
show() { show() {
this.button.show(); this.button.show();
} }
render(): JQuery { /**
const result: JQuery = $("<div></div>").html(window_html); * Создать окно настроек
const row_template = $("#april-fools__window-row", result).html(); * @return {JQuery<HTMLDivElement>}
const joke_container = $(".april-fools__jokes", result); */
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)) { for (const joke of Object.values(this.joker.jokes)) {
const row = $(eval("`" + row_template + "`")); const row = $(eval('`' + rowTemplate + '`'));
$('input[type="checkbox"]', row) $('input[type="checkbox"]', row)
.prop("checked", joke.settings.enabled) .prop('checked', joke.settings.enabled)
.on("click", (e) => .on('click', (e) => {
this.joker.toggleJoke(joke.id, (<any>e).currentTarget.checked) const target = (e.currentTarget as HTMLInputElement);
this.joker.toggleJoke(joke.id, target.checked);
}
); );
joke_container.append(row); jokeContainer.append(row);
} }
$("button", result).on("click", async () => { $('button', result).on('click', async () => {
await this.joker.clearUserStates(); await this.joker.clearUserStates();
window.location.reload(); window.location.reload();
}); });
return $(".april-fools__content", result); return $('.april-fools__content', result);
} }
} }
+21
View File
@@ -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 };
+18 -37
View File
@@ -1,12 +1,11 @@
// Generated using webpack-cli https://github.com/webpack/webpack-cli
const path = require("path"); const path = require("path");
const webpack = require("webpack"); const webpack = require("webpack");
const BomPlugin = require("webpack-utf8-bom"); const BomPlugin = require("webpack-utf8-bom");
const { exec } = require("node:child_process");
const TerserPlugin = require("terser-webpack-plugin"); 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 = { const config = {
entry: "./src/main.ts", entry: "./src/main.ts",
@@ -16,26 +15,25 @@ const config = {
}, },
plugins: [ plugins: [
new BomPlugin(true), new BomPlugin(true),
{ libBanner,
apply: (compiler) => { libUpload,
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.");
});
});
},
},
// Add your plugins here
// Learn more about plugins from https://webpack.js.org/configuration/plugins/
], ],
module: { module: {
rules: [ rules: [
{ {
test: /\.(ts|tsx)$/i, test: /\.tsx?$/i,
loader: "ts-loader", 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, test: /\.(eot|svg|ttf|woff|woff2|png|jpg|gif)$/i,
@@ -45,8 +43,6 @@ const config = {
test: /\.html$/i, test: /\.html$/i,
loader: "html-loader", loader: "html-loader",
}, },
// Add your rules for custom modules here
// Learn more about loaders from https://webpack.js.org/loaders/
], ],
}, },
resolve: { resolve: {
@@ -64,6 +60,7 @@ const config = {
terserOptions: { terserOptions: {
compress: true, compress: true,
}, },
extractComments: false,
minify: TerserPlugin.uglifyJsMinify, minify: TerserPlugin.uglifyJsMinify,
})], })],
}, },
@@ -77,19 +74,3 @@ module.exports = () => {
} }
return config; return config;
}; };
/*
{
"compilerOptions": {
"module": "AMD",
"lib": ["es2017", "dom"],
"rootDir": "./src/",
"outFile": "dist/april-fools.js",
"moduleResolution": "nodenext",
"esModuleInterop": true
},
"files": [
"src/main.ts"
]
}
*/
+16
View File
@@ -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 };